Vue中v-for不加key引发的问题及原因【深度解析】


theme: channing-cyan

v-for 不加 Key 或者用 Index 作为 Key 导致的复用更新问题

背景:

日常开发中,因为v-for不加key或者直接用数组索引做key有时候会产生元素错乱问题 有时候不加key,也不会有问题。有时候却会有问题。
但是如果任何渲染列表的情况都加唯一key。那么有时候一些简单的结构是没有唯一key的。需要自己生成。这就增加了心智负担和不必要的代码。

本文深入解析不加key会产生问题的情况原因及解决方法(反之能知道什么时候可以不加key)。理解原理后写出更健壮的代码。

注:不加key或者用数组索引做key。虽然走的diff算法不同(有key走patchKeyedChildren(快速diff算法),没key走patchUnkeyedChildren)(简单diff算法),但是都会产生一样的问题,因为都是通过数组索引找到新旧元素,找到的元素可能不是同一个元素。

本文解析用patchUnkeyedChildren算法,也就是不加key的情况作为测试用例。

patchUnkeyedChildren 算法步骤

  1. 找到新旧节点子节点的最短长度。

  2. 遍历最短长度的节点。新节点替换旧节点。

  3. 处理剩余的节点:

  • 如果新节点长度大于旧节点长度,挂载剩余的新节点。

  • 如果新节点长度小于旧节点长度,卸载剩余的旧节点。

这个过程实际上就是两个数组之间的对比和操作,通过索引来进行逐个元素的比较和处理

不加 Key 导致的问题解析

文本节点的情况【无问题】

例如:

  • 旧节点:[1,2,3]
  • 新节点:[1,3]

按照上述算法:

  • 旧节点 1 和新节点 1 对比,相同,不更新。
  • 旧节点 2 和新节点 3 对比,相同(根据索引key,误认为是同一个节点),新节点 3 的值更新到旧节点 2。(这里的更新单纯的 el.textContent='新的值')
  • 旧节点 3 被卸载。

结果:

  • 旧节点变为 [1,3]

有自己状态的组件或临时 DOM(非受控输入框)的情况

组件的对比主要是 props 是否有变化。例如:

  • 旧组件:[1,2,3]
  • 新组件:[1,3]

按照上述算法:

  • 旧组件 1 和新组件 1 对比,相同,不更新。
  • 旧组件 2 和新组件 3 对比,props 不同,新组件 3 的 props 传给旧组件 2旧组件2并没有被删除,相同类型的dom,会被复用),又因为props是响应式对象,它的修改会触发使用了它值的页面(执行渲染函数)。
  • 针对有自己状态的组件:props的更新虽然重新触发了渲染函数。但是setup不会再次执行。所以组件的状态保留了下来。
  • 针对有自己状态的临时dom(非受控输入框):props的更新虽然重新触发了渲染函数。但是setup不会再次执行。所以状态保留了下来。 旧组件 3 被卸载。

结果变为:

  • 新组件 [1,2],旧组件 3 被卸载。

具体原因

原因是复用 DOM 结构(dom类型一致就复用)只更新属性。例如:

  • input 内部维护的值无法更新。
  • 组件内部维护的值依赖组件的 props,但组件内部状态的初始化仅在组件初始化时执行。

解决方法

使用唯一 Key

  • 加了唯一 key,新旧节点可以找到对应的节点(相同节点)。这样,即使组件有自己的状态,也只需更新 props,不会出现问题。
  • 不加 key 或使用索引作为 key 时,找到的新旧节点可能不是相同节点

测试用例

基于上述的结论。进行代码验证。使问题出现。

临时dom问题复现【会出现问题】

依次在输入框输入1 2 3 jcode image.png 删除中间元素。结果符合上面的分析。

image.png

列表内子组件有自己的状态 【会出现问题】

依次在输入框输入1 2 3 jcode image.png 删除中间元素。结果。主要看 组件自己的状态,不会更新:2。 可以证明这是旧节点。
被复用了。并且只更新了props

临时dom改成受控模式【不会出现问题】

依次在输入框输入1 2 3 jcode image.png 删除中间元素。不会出问题。 2被正确的删除掉了。 image.png

解析代码:


  
<!-- 下面的input形如: 可以看到text绑定了:value。那么text的更新会触发input的重新渲染。 --> 删除当前元素

日常开发中也要注意
组件的props是响应式的。如果用在模板(编译后会变成渲染函数)上。 props修改会触发副作用函数(渲染函数)重新渲染页面。 但是setup里面的代码只会在组件初始化也就是created前执行一次。
如果基于props的值初始化定义了响应式数据并且使用到了模板上。props的值改变并不会重新出发渲染函数。
这个情况可以通过watch监听props的值进行处理。

总结

不加 key 或使用索引作为 key,(列表内的组件有自己状态 或者 临时dom(就是非受控组件/元素))会导致更新错误:

临时dom:
标签 内部会维护自己的输入值状态,input 内部维护的值无法得到更新。

有自己的状态的子组件
组件更新的时候主要替换props。

  • 替换 props,更换属性。
  • 如果组件内部状态依赖 props 初始化,随后渲染到页面,更新 props 只会触发页面的渲染,而 setup 函数仅在组件初始化时执行。

这是一个从 https://juejin.cn/post/7369124076067995675 下的原始话题分离的讨论话题