从底层看 Vue 的 key 到底有什么用

320 阅读4分钟

Vue 的 key 到底有什么用?

结论

key 属性的主要作用是对元素加上了一个唯一标识,主要作用在 Vue 的 diff 算法,在新旧 nodes 对比时辨识 VNodes。在 v-for 循环出来的元素发生变化时,diff 算法中会使用 key + 元素标签作为 diff 条件,找到需要更改的真实 DOM 部分,从而进行局部更新,而不是 DOM 全量更新。所以 key 属性的独特性对于性能优化和避免潜在的渲染错误至关重要。

v-for 的 key 可以不设置 key 吗?

先说结论:对于静态列表可以,对于动态列表可能会导致渲染错误。

下面来看一个案例:

<template>
  <div>
    <ul>
        <li v-for="(item, index) in listData">
            <input type="text">
            <button @click="deleteProperty(index)">删除</button>
        </li>
    </ul>
  </div>
</template>
<script>
export default {
    name: 'key',
    data() {
        return {
            listData: [
                {
                    id: 11,
                    value: 1
                },
                {
                    id: 22,
                    value: 2
                },
                {
                    id: 33,
                    value: 3
                },
                {
                    id: 44,
                    value: 4
                },
                {
                    id: 55,
                    value: 5
                }
            ]
        }
    },
    methods: {
      	// 删除元素操作
        deleteProperty(index) {
            this.listData.splice(index, 1);
        }
    }
}
</script>

代码执行完毕后,我们可以看到效果:

image.png

此时我们点击上图所示的删除按钮,然后查看结果:

image.png

会发现,我们删除的是 3 。但是看效果,实际是 5 被删除了,但是 3 还是存在的。这是为啥呢?

原因:

在 vue 里的 diff 算法中有一个判断节点是否相同的逻辑:

image.png

下面解释一下相关变量与方法:

a:VNode1
b:VNode2
key:节点所绑定的key
sel:节点标签名

这里讲一个注意点:当节点没有 key 的时候,这时的 key 默认是 undefined,如果前后变更的节点都没有 key 存在且元素标签名相同时,就判断这两个节点是相同的。

然后我们接着往下看 diff 过程:

image.png 旧 为更新前的 VNodes,新 为更新后的 VNodes,因为我们删除了第 3 个节点,所以 新 就只剩下 4 个节点了。

首先新旧 1、2 节点是没有变化的,所以保持原样。

我们来看旧节点的 3 节点与新节点的 4 节点对比,首先根据上面 diff 算法中判断节点是否相同的逻辑来看,这两者的共同点为:

1、sel 为 li 2、key 为 undefined 所以这两个节点满足节点相同的条件。

此时我们就要进行对比这个元素里的子节点,子节点的相同点为: 1、sel 为 input 2、key 为 undefined 此时这两个子节点也满足节点相同的条件。

那么也就是说这两个节点都是完全一样的,在 diff 算法中的处理就是 DOM 元素复用,所以新 4 元素将不会进行任何修改,直接复用旧 3 元素。

然后旧节点的 4 节点与新节点的 5 节点对比,情况也是同上。新 5 元素将不会进行任何修改,直接复用旧 4 元素。

最后还剩下一个旧 5 元素,发现没有可对比的新元素了,所以这里直接进行了删除。(这就是为什么原本删除了 3 元素,但是最后面却发现删除了 5 元素)

然后我们使用 key 后,再尝试一下:

<li 
   v-for="(item, index) in listData"
   :key="item.id"
>
   <input type="text">
   <button @click="deleteProperty(index)">删除</button>
</li>

image.png

发现正常了,这里的 diff 过程如下:

image.png

使用了 key 绑定后,新旧节点都能各自匹配上。匹配完毕后发现 3 是多余的,故能正常删除 3 元素。

2)v-for 的 key 可以设置索引吗?

先说结论:对于静态列表完全可以,对于有增删的场景的列表,同样会导致 diff 错误。

还是使用上面的案例,key 的值改成 index:

<li 
   v-for="(item, index) in listData"
   :key="index"
>
   <input type="text">
   <button @click="deleteProperty(index)">删除</button>
</li>

还是,执行删除第 3 个元素,这里贴一个新旧节点的 key 对比表格

旧节点的 key新节点的 key
00
11
22
33
4-

因为 key 是每次动态生成的,不具有唯一性,所以当元素数量变小的时候,对应的元素的 key 值可能会改变。所以这里也会导致更新错误的问题