探究Vue3 v-for列表节点不必要重渲染的成因与场景差异
Vue 列表不必要重渲染的底层原理拆解
所有测试结果的差异,本质都来自Vue的两个核心运行规则:
- 同模板作用域内的v-for节点,没有独立的组件更新边界,只要作用域内的响应式值更新,整个作用域的渲染逻辑会重新执行
- 跨组件边界的props更新、响应式依赖触发,遵循浅引用比较逻辑,只要依赖收集到的响应式值发生变更,就会触发对应watcher的更新流程,不会因为最终计算的结果值相同就跳过更新
各测试场景的触发原理与对应逻辑
测试1:同作用域v-for全量重渲染
- 触发原因:所有v-for渲染的节点都直接写在父组件的渲染函数作用域内,当控制循环长度的
n值变化时,整个父组件的渲染函数会被触发重新执行。这个过程中哪怕某一项的索引值完全没有变化,对应的插值、自定义指令的绑定值都会被重新求值执行,甚至在没有静态提升、块级缓存优化的场景下,会因为重新生成的vnode树结构匹配问题,触发原有节点的销毁、重建流程,表现为全量节点都触发了重渲染的钩子。 - 规避方法:将v-for单节点的渲染内容拆分为独立子组件,把动态值通过props传入,让单节点的渲染逻辑脱离父组件的全局渲染作用域。
测试2:拆分独立组件后重渲染消失的原因
将循环内容抽到独立组件后,父组件的渲染函数重跑时,只会生成子组件的vnode,对传入的基础类型propi做浅比较:因为i是数字类型,值没有变化,Vue就会直接跳过子组件的内部渲染流程,不会重复执行子组件的渲染逻辑,自定义指令的钩子自然也不会重复触发。
测试3:循环内动态构造对象传参的重渲染验证
对“动态构造对象传参导致重渲染”的推测完全成立。父组件渲染函数重跑时,每次执行{ i: { i: { i } } }对象字面量都会生成一个全新的对象引用,哪怕内部嵌套的属性值完全一致,因为引用地址变化,Vue对props的浅比较逻辑就会判定prop发生变更,直接触发子组件的重渲染。
测试4:直接传Vuex对象不触发重渲染的原因
this.$store.state.items中的每个item对象,是store初始化时就生成的固定引用。父组件渲染重跑时,遍历item直接把原对象的引用传给子组件,没有重新构造对象,浅比较时发现传入的引用和上一次渲染完全一致,就会跳过子组件的更新流程,不会产生不必要的重渲染。
测试5:动态重构对象传参触发重渲染的原因
和测试3的逻辑完全一致:父组件渲染重跑时,{ id: item.id, name: item.name }每次都会生成一个全新的对象,哪怕属性值和原item完全一致,因为引用地址变化,子组件的props浅比较不通过,就会触发重渲染。
测试6:子组件内部computed取store数据仍触发重渲染的原因
这个场景和测试4的核心差异来自响应式依赖的收集范围:
- 子组件内部的computed在求值时,执行了
this.$store.state.items.find(...)的逻辑,这个过程会自动收集items数组本身的变化作为依赖 - 当父组件向
items数组新增元素时,数组的length属性变更,触发所有收集了数组依赖的watcher重新计算 - 哪怕本次computed重新执行时,
find返回的还是和之前完全一致的对象引用,computed对应的watcher依然会向组件派发更新通知,导致组件的渲染函数重新执行,触发对应的节点钩子,表现为不必要的重渲染 - 测试4的子组件因为是直接接收父组件传入的item对象,没有在内部computed中遍历整个items数组,自然不会收集到items数组本身的变化,也就不会因为数组新增元素触发自身的更新
内容的提问来源于stack exchange,提问作者Kasheftin
相关产品推荐
相关产品推荐

