Vue列表渲染的key的作用

284 阅读4分钟

本文会从以下这几个方面来说明对于Vue列表渲染,什么场景下key必须是唯一值不能使用index,什么场景下key可以使用index?

动态列表: key为index:
动态列表: key为唯一值:
静态列表: key为index:
静态列表: key为唯一值:
下面就直接跟着源码debugger来看看具体的diff流程是如何走的吧~
看之前我们需要知道diff的流程是基于vue的整个生命周期的哪个阶段的,可以参考下图:
image.png 在vue中无论是首次渲染还是后面的响应式更新都是会走到内部patch函数,所以因为我们本次的目标就是diff流程,所以其他的函数暂时不在我们的关注的范围内(每次调试要明确我们的目标,如果目标不明确的话很容易调试的过程中就被其他的一些case给吸引了,导致最后都忘了本地要调试的目标或者说要解决的问题是啥了),patch函数:

image.png

image.png 上面贴的两张图是当我们更新完数据后(本次的操作就是向列表的头部新添加了一个元素后),第一次走到patch函数的截图,oldVnode和vnode是前后两次虚拟dom对象,此时代表的是整个dom的根对象,也就是id为app的元素,因为我们要观察的是ul元素,所以我们直接跳到ul及其孩子元素的patch中:

image.png 可以在上图中看到:newCh就是新vnode,是有四个元素,第一个其实就是我们因为改变了响应式数据而添加的新的元素,oldCh就是老的vnode,它是有三个元素的, 下面画了一张图来标识一下现在的vnode状态: 欠一张图片:

下面的这张图是整个diff的过程中的所有case, image.png

下面就从文章一开头说的那四种情况看下具体的更新策略:
对于动态列表-key是唯一值的场景来说:diff流程是这样的:
  • 第一步:开始比对ul的children列表,走到updateChildren函数
  • 第二步:比较newCh的头节点和oldCh的头节点,使用sameVnode判断,此时我们newCh的第一个节点key是4,oldCh的头节点是1,不匹配走到下一步。
  • 第三步:比较newCh的尾节点和oldCh的尾节点,发现一样,开始递归的patch其孩子,如果孩子也是个数组会递归的执行上面的步骤。
  • 第四步:继续执行第三步,直到比对到newCh的第一个节点,此时oldCh已经比对结束,没有节点了,
  • 第五步:走到if(oldStartIdx > oldEndIdx)的逻辑,执行将newCh的第一节点插入到其上一个节点的逻辑;
    总:所以我们可以看到,在动态列表中,key是唯一值,只需要更新一次。

动态列表-key是唯一值.gif

对于动态列表-key是index的场景来说:diff流程是这样的:

此时newCh的key依次是1、2、3、4, oldCh的key依次是:1、2、3

  • 第一步:比对newCh的头节点和oldCh的头节点,sameVnode返回true, 因此开始比对他们及孩子,因为孩子不相同所以会此时更新一次
  • 第二步:继续比对,依然是和第一步的逻辑相同的步骤,更新第二个,
  • 第三步:继续比对,更细第三次,
  • 第四步:此时newCh还有一个元素没有比对, oldCh没有元素了,走到if(oldStartIdx > oldEndIdx)的逻辑,执行将newCh的第一节点插入到其上一个节点的逻辑,更新第四次。
    总:可以看到在使用index作为key的场景中,总共更新了四次。

动态列表-key是index.gif

对于静态列表-key是唯一值,diff流程是这样的:
  • 第一步:比较newCh的头节点与oldCh的头节点,相同:不更细,直接复用
  • 第二步:重复第一步, 直到比对完所有的孩子 总:对于静态列表-key是唯一值的场景,更新了0次

静态列表-key是唯一值.gif

对于静态列表-key是index,diff流程是这样的:
  • 第一步:比较newCh的头节点与oldCh的头节点,相同:不更细,直接复用
  • 第二步:重复第一步, 直到比对完所有的孩子
    总:对于静态列表-key是index的场景,更新了0次
    静态列表key是index.gif 总结:通过上面的流程我们可以看到,对于动态列表来说,key保证是唯一值确实对更新的性能是有提升的,而对于静态列表其实key是用index还是唯一值是没有区别的。

参考文章: 如果debugger单步调试Vue源码