1. 官方文档有关key的说明
key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。 而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
2. 举个例子
可以运行一下代码,改变key为 索引idx(或者不设置key) 或者 item.id, 看下效果
- 当索引idx,作为key的时候(或者不设置key),选中一项,再点击添加新值的时候,选中的项会改变
- 当item.id作为key的时候,选中一项,再添加的时候,选中项不会变化
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<div>
<input type="text" v-model="name">
<button @click="add">添加</button>
</div>
<ul>
<!-- <li v-for="(item,idx) in list" :key="idx"> -->
<li v-for="item in list" :key="item.id">
<input type="checkbox">ID:{{item.id}} -------name:{{item.name}}
</li>
</ul>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
name: '',
list: [{
id: 0,
name: '数据1'
},
{
id: 1,
name: '数据2'
},
{
id: 2,
name: '数据3'
}
]
},
methods: {
add() {
const newUser = {
id: this.list.length,
name: this.name
};
this.list.unshift(newUser);
}
}
})
</script>
</body>
</html>
可以看出,当key值为item.id的时候,checkbox选中项在添加新项的时候不会变化。为什么会这样呢?我们往下看
3. diff
在vue中,当数据发生变化,会触发更新,更新过程的核心就是新旧 vnode的diff。
diff的原理就是对前后的节点树同一层的节点进行对比,一层一层对比,如下
当某一层有很多相同的节点时,也就是列表节点时,diff算法的更新过程默认情况下也是遵循以上原则。
如下
我们希望可以在B和C之间加一个F,diff算法默认执行起来是这样的
即把C更新成F,D更新成C,E更新成D,最后再插入E,相当没有效率!
所以我们需要使用key来给每个节点做一个唯一标识,diff算法就可以正确的识别此节点,找到正确的位置去插入新的节点。如下图
在vue中,判断新旧节点的sameVnode(oldVnode, vnode)函数如下
js
function sameVnode(a, b) {
return (
a.key === b.key &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)))
)
}
可以看到,判断两个节点是否一样,首先判断了节点的key
因此,唯一的key值,会在diff比较新旧节点是否相同时,起到关键作用,起到更高效的更新虚拟节点的作用
4. 为什么不推荐索引index作为key
可以参考例子中的代码,在增删变化的场景中,元素的index可能是会变化的,diff算法时比较同级之间的不同,以key来进行关联,当对数组进行下标的变换时,比如删除第一条数据,那么以后所有的index都会发生改变,那么key自然也跟着全部发生改变,所以index作为key值是不稳定的,而这种不稳定性有可能导致性能的浪费,导致diff无法关联起上一次一样的数据。因此,能不使用index作为key就不使用index
5. 为什么不要用随机数作为key?
js
<item
:key="Math.random()"
v-for="(num, index) in nums"
:num="num"
:class="`item${num}`"
/>
其实我听过一种说法,既然官方要求一个 唯一的key
,是不是可以用 Math.random()
作为 key
来偷懒?这是一个很鸡贼的想法,看看会发生什么吧。
首先 oldVnode
是这样的:
js
[
{
tag: "item",
key: 0.6330715699108844,
props: {
num: 1
}
},
{
tag: "item",
key: 0.25104533240710514,
props: {
num: 2
}
},
{
tag: "item",
key: 0.4114769152411637,
props: {
num: 3
}
}
];
更新以后是:
js
[
{
tag: "item",
+ key: 0.11046018699748683,
props: {
+ num: 3
}
},
{
tag: "item",
+ key: 0.8549799545696619,
props: {
+ num: 2
}
},
{
tag: "item",
+ key: 0.18674467938937478,
props: {
+ num: 1
}
}
];
可以看到,key
变成了完全全新的 3 个随机数。
上面说到,diff
子节点的首尾对比如果都没有命中,就会进入 key
的详细对比过程,简单来说,就是利用旧节点的 key -> index
的关系建立一个 map
映射表,然后用新节点的 key
去匹配,如果没找到的话,就会调用 createElm
方法 重新建立 一个新节点。
js
// 建立旧节点的 key -> index 映射表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
// 去映射表里找可以复用的 index
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 一定是找不到的,因为新节点的 key 是随机生成的。
if (isUndef(idxInOld)) {
// 完全通过 vnode 新建一个真实的子节点
createElm();
}
也就是说,咱们的这个更新过程可以这样描述:123
-> 前面重新创建三个子组件 -> 321123
-> 删除、销毁后面三个子组件 -> 321
。
这样一来创建新的组件和销毁组件的成本就非常的大,本来仅仅是对组件移动位置就可以完成的更新,被我们毁成这样了。
总结
- 用组件唯一的
id
(一般由后端返回)作为它的key
,实在没有的情况下,可以在获取到列表的时候通过某种规则为它们创建一个key
,并保证这个key
在组件整个生命周期中都保持稳定。 - 别用
index
作为key
,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是0, 1, 2
这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。 - 千万别用随机数作为
key
,不然旧节点会被全部删掉,新节点重新创建。