本文续接上篇 我终于搞懂了vue的diff算法
引用这篇掘金文章中所列出的两种情况,本文只对分析过程做一些图示补充
<div>
<span v-for="(item,index) in arr" :key="index" >{{item}}span>
div>
当使用index作为key时,首先生成的旧虚拟dom节点层级为:
div
span A key = 0
span B key = 1
span C key = 2
span D key = 3
span E key = 4
生成的新虚拟dom节点层级为
div
span E key = 0
span D key = 1
span C key = 2
span B key = 3
span A key = 4
span A key = 0 代表 span 标签内的文本是A,span 标签的 key 是 0,以下同理
A
和新节点 E
E
那么正常以唯一标识作为key的情况是怎样呢,假设我们以值作为key,代码如下
<div>
<span v-for="item in arr" :key="item" >{{item}}span>
div>
span E
也就跑到了 span A
之前。所以看起来key用index,并没什么区别啊,反正最终结果是对的。
但实际上用index作为key的时候,我们去更新了dom元素,更新了节点的文本,但用唯一标识作为key时,只是简单的复用了dom,移动了dom的位置而已
假设有如下代码: arr = ['A', 'B', 'C', 'D', 'E']
<div v-for="(item, index) in arr" :key="index">
<demo></demo>
</div>
demo组件
<template>
<div>{{ Math.random() }}</div>
</template>
假设Math.random()生成的随机数依次为1,2,3,4,5。
现在我们执行 arr.splice(0, 1)
,删除数组的第一个元素 A 。理想情况下,页面展示的应该是 2,3,4,5,但实际上页面展示的却是 1,2,3,4
生成的旧虚拟dom节点层级为:
demo 1 key = 0 代表 demo 组件内展示的随机数是 1,demo 标签的 key 是 0,以下同理
div
demo 1 key = 0
demo 2 key = 1
demo 3 key = 2
demo 4 key = 3
demo 5 key = 4
生成的新虚拟dom节点层级为
div
demo 2 key = 0
demo 3 key = 1
demo 4 key = 2
demo 5 key = 3
比较第一层,即从div开始比较,div光秃秃的啥都没有,所以符合sameVnode,调用patchVnode
新节点中不存在文本,新旧节点都存在子节点(本例中为子节点为 demo 标签),调用updateChildren,传入子节点 即 demo 标签构成的子节点数组
按图中的五种逻辑分支比较依次进行比较,由于key是index,他们的key相等,因此第一个比较分支就符合sameVnode(key相同都是0,tag相同都是demo),(到此对应图中第一条绿色线)。
然后调用patchVnode,此时传入的两个节点分别为
旧节点demo 1 key = 0
和新节点 demo 2 key = 0
此时走 patchVnode 逻辑,新旧节点内都不存在文本,紧接着走后面的四步逻辑判断,发现四步都不命中,于是本次更新什么都不做。
那么本次更新后相当于直接复用了旧节点demo 1 key = 0
作为页面展示的第一个元素。
后面依次调用,由于新的虚拟dom节点中demo节点只有4个,所以本次循环以新节点率先循环完毕而结束,循环结束时旧节点还剩一个demo 5 key = 4
没有比对,直接删除多余的节点。
最终更新后的节点就变成了
div
demo 1 key = 0
demo 2 key = 1
demo 3 key = 2
demo 4 key = 3
页面展示 1,2,3,4,本来要删除1的,结果错误的把5删掉了
<div v-for="(item, index) in arr" :key="item">
<demo></demo>
</div>
div
demo 1 key = A
demo 2 key = B
demo 3 key = C
demo 4 key = D
demo 5 key = E
生成的新虚拟dom节点层级为
div
demo 2 key = B
demo 3 key = C
demo 4 key = D
demo 5 key = E
demo 2 key = B
移动到了首位。导致删除错误的原因,在于使用了index作为key,在updateChildren逻辑中,比较新旧节点的首节点,由于key是index,导致sameVnode在判断时认为两个节点是相似的,然后错误复用了节点。