Vue v-for列表子组件修改Vuex状态单项后如何避免全量重渲染
全量子组件触发更新的核心原因
- 你当前使用的
questionListParsed是Vuex中的响应式getter,只要它依赖的任何响应式状态(questionList、showingOriginalQuestion、removedRecommendQuestionIds)发生变化,整个getter会被全量执行一次 - getter内部通过
map遍历所有原始题目,为每一项题目重新构造全新的解析对象、嵌套属性对象——哪怕某道题的源数据完全没有改动,生成的新对象和上一次渲染的对象引用地址也是完全不同的 - 你给子组件绑定的
:both-question="item"是对象类型的prop,Vue对对象类型的props默认做浅引用比较:只要引用地址变了,就会判定prop发生更新,进而触发子组件的updated钩子执行 - 你设置的
:key="item.id"仅用于虚拟DOM的节点复用,减少真实DOM的创建销毁开销,并不会阻断props变化触发的组件更新流程,因此无法避免所有子组件被判定为需要更新。
可落地的优化方案
1. 优化getter计算逻辑,为未变化的列表项保留原对象引用
核心思路是在getter层加一层缓存机制,不要每次全量生成新对象,只有真正发生源数据变化的项才重新构造解析对象,其他项直接复用上一次的解析结果引用。
参考实现步骤:
- 在state中新增缓存字段
questionParsedCache: {},用于存储每道题已经解析好的结果 - getter执行遍历原始题目列表时,先判断当前题目是否满足「源数据引用变化」「关联的推荐题状态变化」两个条件,满足才重新生成解析对象;不满足就直接从缓存中取之前的解析对象返回
- 遍历完成后清理已经不在列表中的过期缓存项
示例代码:
get questionListParsed(): QuestionListItemRes[] { const { questionList, showingOriginalQuestion, removedRecommendQuestionIds, questionParsedCache } = this const activeCacheKeys = new Set() const parsedList = questionList.map((rawQuestion) => { const itemKey = rawQuestion.questionNumber activeCacheKeys.add(itemKey) const cachedData = questionParsedCache[itemKey] // 判定当前项是否需要重新解析: // 无缓存、缓存绑定的源引用和当前最新源数据不一致、关联的推荐题删除状态有变化 const relatedRecommendIds = showingOriginalQuestion[itemKey] || [] const recommendStateChanged = removedRecommendQuestionIds.some(id => relatedRecommendIds.includes(id)) const sourceChanged = cachedData?._rawRef !== rawQuestion if (cachedData && !sourceChanged && !recommendStateChanged) { return cachedData } // 仅对需要更新的项执行原有的解析逻辑 const recommendQuestion = rawQuestion.recommendedQuestions[0] const validRecommendIds = relatedRecommendIds.filter(id => !removedRecommendQuestionIds.includes(id)) const newParsedData = { recommendQuestion: { ...recommendQuestion, stem: recommendQuestion.question, knowledges: splitMultiKnowledge(recommendQuestion.knowledge), questionSourceList: recommendQuestion.sources, categoryId: recommendQuestion.categoryId, }, originalQuestion: { ...rawQuestion, id: rawQuestion.questionNumber, stem: rawQuestion.question, difficulty: rawQuestion.complexity, knowledges: splitMultiKnowledge(rawQuestion.knowledge), }, id: recommendQuestion.id, questionSimilarId: rawQuestion.questionNumber, mistakeAnswerId: rawQuestion.id, targetExerciseId: rawQuestion.targetExerciseId, status: recommendQuestion.status, _rawRef: rawQuestion, // 缓存原始引用,用于后续变化判断 } // 更新对应缓存 this.questionParsedCache[itemKey] = newParsedData return newParsedData }) // 清理已被移除题目的过期缓存 Object.keys(questionParsedCache).forEach(key => { if (!activeCacheKeys.has(Number(key))) { delete questionParsedCache[key] } }) return parsedList }
完成这层改造后,未发生变化的题目项返回的永远是同一个对象引用,子组件的props浅比较时会判定无变化,自然不会触发更新。
2. 配合v-memo实现组件更新的精准跳过
如果使用的是Vue2.7+或者Vue3版本,可以在子组件的渲染标签上加v-memo指令,把真正影响组件渲染的原始值作为依赖数组。哪怕整个item对象被重新构造,只要数组里的原始值没有变化,就会直接跳过该子组件的更新流程:
<question-list-item v-for="(item, index) in questionListParsed" :key="item.id" :both-question="item" :class-id="classId" :subject-id="subjectId" :index="index+1" v-memo="[ item.id, item.questionSimilarId, item.mistakeAnswerId, item.status, item.originalQuestion.stem, item.recommendQuestion.stem ]" />
注意v-memo的依赖数组里不要放整个item对象,否则引用变化依然会触发更新。
3. 拆分计算逻辑,缩小更新影响范围
可以把原本在getter中完成的题目解析逻辑下沉到question-list-item子组件内部,父组件的列表只传递原始的questionList项作为prop。这样某一项的原始数据变化时,只有对应位置的子组件收到新的prop,其他子组件的原始prop引用完全不变,从机制上避免全量更新。
额外排查点
- 检查传入子组件的其他props,比如
classId、subjectId是否存在每次渲染都生成新引用的情况,如果有也要做缓存优化 - 检查子组件是否直接订阅了Vuex的全局状态,在组件内部watch了全量state变化,这种情况哪怕父组件传递的props没有变化,也会触发组件更新
内容的提问来源于stack exchange,提问作者wuhuoshao
相关产品推荐
相关产品推荐

