这是一篇脱坑日记,在做项目的过程中,我使用了 v-for 渲染子组件时,并将 index 绑定给了 key,这一行为导致删除操作会误删子组件,实际上删除的组件并不是你预期的那个。而且我在排查错误的过程中打印 log 的数据信息均正常,唯独在执行删除操作时出现异常。
深陷其坑而不知具体原因,询问大佬后恍然大悟,究其根源还是自己太年轻,只知道使用 v-for 的时候需要绑定 key,却不知道在复杂情况下不能用 index 作为 key,特此总结这篇博客。
1、先来看一个例子
假设你有三个子组件,每个子组件里面有一个「有状态的」孙子组件。现在用户点击删除按钮,把第二个子组件删掉了,请问结果是怎样的?你可能会说,那还用问?当然是 2 消失了,因为 data 里的数组从 [1,2,3] 变成了 [1,3]。实际上你没有考虑全面:注意看图中的绿色正方形没有被删除。
原因很简单,你认为你删除了 2,但 Vue 会认为你做了两件事:(1)把 2 变成了 3、(2)然后把 3 删除。
Vue 为什么要舍近求远呢?看看这两个数组:[1,2,3] 和 [1,3]。人类会说,这不就是少了个 2 吗?但是计算机会怎么对比数组?遍历!首先对比 1 和 1,发现 [ 1 没变 ];然后对比 2 和 3,发现 [ 2 变成了 3 ];最后对比 undefined 和 3,发现 [ 3 被删除了」。所以计算机的结论是:[ 2 变成了 3 ] 以及 [ 3 被删除了 ]。
既然 [1 没变】,那么就地复用之前的 1 和三角形就好了。
既然 [ 2 变成了 3 ],那么正方形左边的 2,当然要改成 3。里面的正方形就地复用(正方形没有被删除)。因为正方形是孙子元素的 data,不受 [ 2 变成 3 ] 的影响,所以可以就地复用。
既然 [ 3 被删除了」,之前的 [圆形] 当然应该被删掉,里面的子元素也要删除。
2、上述问题的解决方法
怎么解决这个问题呢?怎么让 Vue 知道我删除的是第二个,不是第三个?用 id 作为 key 就行了,不信你再看:
我们以计算机的角度来思考一下:原本的数组是 [{id:1,value:1},{id:2,value:2].{id:3,value:3],点击删除之后的数组是 [{id:1,value:1},{id:3,value:3}],Vue 会在删除操作后执行 DOM diff 算法,对比前后两次 dom 节点的异同:
首先发现 id 从 1 2 3 变成了 1 3,说明第二项被删除了
然后依次对比 id:1 的项和 id:3 的项,发现删除前后这两个节点没变化。
所以计算机得出结论:第二项被删除了。符合人类预期!代码示例
3、高效的 Diff 算法原理
其实不只是 Vue,React 中在执行列表渲染时也会要求给每个组件添加上 key 这个属性。要解释 key 的作用,不得不先介绍一下虚拟 DOM 的 Diff 算法了。Vue 和 React 都实现了一套虚拟 DOM,使我们可以不直接操作 DOM 元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的 Diff 算法。
Vue 和 React 的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设:
1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
2. 同一层级的一组节点,他们可以通过唯一的id进行区分。
基于以上这两点假设,使得虚拟 DOM 的 Diff 算法的复杂度从 O(n3) 降到了 O(n)。看图分析:
当页面的数据发生变化时,Diff 算法只会比较同一层级的节点:
如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。
如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。比如下面这种情况,我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:
即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
所以一句话,key 的作用主要是为了高效的更新虚拟 DOM。另外 Vue 中在使用 相同标签名元素的过渡切换 时,也会使用到 key 属性,其目的也是为了让 Vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
4、不能用 index 作为 key
为什么不能用 index 作为 key,如果你用 index 作为 key,那么在删除第二项的时候,index 就会从 1 2 3 变成 1 2(而不是 1 3),那么 Vue 依然会认为你删除的是第三项。也就是会遇到上面一样的 bug。
所以,永远不要用 index 作为 key。永远不要!除非你是大神。能清楚地知道如何解决 index做 key 带来的 bug。有人说简单的场景可以用 key。问题在于,你如何确保需求会一直保持简单?只要出现了删除一项或新增一项的需求,而且这一项里面含有子组件,上面说的 bug 就有可能出现。