问题简述
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
):
- 对比新旧 vnode,若有 key,则找一找有没有可以复用的
- 发现
"张三", "李四", "王五"
这三个 key 可以复用,而且内容相同,所以不做修改 - 发现
林六
这个 key 找不到对应的旧结点,于是新增该结点 - 操作一次 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
为了方便学习,你可以直接下载 源代码 节省一点时间