Vue 不建议使用 index 作为 key

问题简述

Vue 不建议使用 index 作为 key,因为会导致一些性能问题,甚至可能产生 bug

<!-- 不建议用 index 作为 key -->
<div>
  <p v-for="(item,index) of list" :key="index">{{item}}</p>
</div>

<!-- list: ["张三", "李四", "王五"] -->

看了若干篇文章,讲的都不是通俗易懂的,自己来总结一下


key 的作用

上面的代码片段在开发中是屡见不鲜的,使用 v-for 来渲染一些同类型的结点,Vue3 文档 里描述了 key 的用途,就是尽可能地复用结点,减少 DOM 操作,提高效率和性能

来看下面的一段伪代码(不能运行),在输入框里输入一些值,点击添加按钮就会在列表顶部添加一条记录

 <div class="list">
   <p v-for="item in list" :key="item">{{item}}</p>
 </div>
<input type="text" v-model="content" placeholder="type name" />
<button @click="addItem">添加项目</button>

<script>
// 来自 vue data
content: "",
list: ["张三", "李四", "王五"]
// 来自 vue methods
addItem() {
  this.list = [this.content, ...this.list];
},
</script>

此时打开浏览器开发者工具,偷偷修改一下真实 DOM,给 王五 添加一个 id 标记,用来检查 Vue 结点复用情况

<div class="list">
  <p>张三</p>
  <p>李四</p>
  <p id="wu">王五</p>
</div>

在输入框里输入 “林六”,然后点击添加,真实 DOM 就会变成:

<div class="list">
  <p>林六</p>
  <p>张三</p>
  <p>李四</p>
  <p id="wu">王五</p>
</div>
如果你的眼神足够犀利,可以看到 Devtools 中的 DOM 闪烁,这里只有林六闪烁了一下,表示 DOM 操作为一次。你完全可以想象去掉 key 的绑定时 DOM 操作为几次呢?操作了 4 次!

实际上 Vue 做了这样的事情(diff):

  1. 对比新旧 vnode,若有 key,则找一找有没有可以复用的
  2. 发现 "张三", "李四", "王五" 这三个 key 可以复用,而且内容相同,所以不做修改
  3. 发现 林六 这个 key 找不到对应的旧结点,于是新增该结点
  4. 操作一次 DOM,完成页面更新

如果没有 key 呢?此时新旧 虚拟 DOM 的对应关系如下:

林六 ------ 张三
张三 ------ 李四
李四 ------ 王五
王五 ------ <empty>

Vue 会做如下事情:

  • 张三更新为林六
  • 李四更新为张三
  • 王五更新为李四
  • 最后新增王五

操作 4 次 DOM 才能完成页面更新,所以添加 key 是十分必要的事情


index 不建议作为 key

不妨先将 key 绑定为 index,也就是文章开头那里的代码片段

<div>
  <p v-for="(item,index) of list" :key="index">{{item}}</p>
</div>

添加林六,然后再次观察 DOM 闪烁情况,发现竟然也闪烁了 4 次!绑定了 key 竟然没有复用任何结点!

因为 index 在向数组 list 添加一个值时发生了变化:

index -- 旧 vnode -- 新 vnode
0 ------ 张三 ------ 林六
1 ------ 李四 ------ 张三
2 ------ 王五 ------ 李四
3 ------ <empty> --- 王五

首先,Vue 尝试复用 key 值相同均为 0 的张三和林六,然后把张三更新为林六
然后,Vue 尝试复用 key 值相同均为 1 的李四和张三,然后把李四更新为张三
诸如此类 ...

所以 Vue 复用结点了吗?复用了,但用错了,index 的变化误导了 Vue,导致 Vue 找错了要复用的结点。

此外,还记得 “偷偷修改 DOM,添加标记” 吗?这种方法可以更加清晰的观察结点复用,可以自己尝试

总结

对于新旧两份数据,如果用肉眼直接看,其他都不动,在张三上面添加个林六问题不久解决了吗?这就是 Vue Diff 的目的所在,也是添加 key 的意义所在。

使用 item 这样不变的值作为 key,结点复用正常运行;
而使用 index 作为 key,由于 index 变化了,所以结点不能正常复用。

当然,上面的举例是一种极端情况,你可能一开始就好奇为什么我在向 list 添加数据时,不是简单 push 而是把新数据放在了数组的 0 号位置。

如果是 push,也就是在数组末尾添加新数据,或者拓展一点,删除旧数据 pop,DOM 的复用也是正常的。因为 index 没有被打乱,新旧虚拟 DOM 仍然正确地一一对应。

Help

为了方便学习,你可以直接下载 源代码 节省一点时间