前言
目标:通过面试题来让自己的知识面更加广,更加深入。
(滴滴、饿了么)写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
key关键词是什么
首先我们从官网的解释出发:


划重点
- key会用在虚拟DOM算法(diff算法)中,用来辨别新旧节点。
- 不带key的时候会最大限度减少元素的变动,尽可能用相同元素。(就地复用)
- 带key的时候,会基于相同的key来进行排列。(相同的复用)
- 带key还能触发过渡效果,以及触发组件的生命周期
上面这么4点就是官方文档中主要解释的意思。吃透了以上几点,也就知道了key是什么以及他的作用。
从源码层面来窥探key在diff算法中的作用
- 判断是否是相同节点的时候

diff算法我们知道,他会判断开始和结尾4个节点是否是相同的节点,而这个sameVnode第一步就是判断两个元素的key值。意味着key相同,它们才有可能是相同元素,不相同,直接返回false。
- 判断是否是相同的静态节点的时候

- 创建key-index的map的时候

这边我只是截取了代码片段,这一步是将vnode的子元素数组,生成key-index的map,方便通过key去查找对应的index子元素,进行相同节点的比较。而它的上文是前后4个新旧节点两两对比不成功的情况下,下文就是通过这个map去定位相同的元素。
带key和不带key,谁的速度更快
聊到速度这个话题,我们先可以从本质上比较一下。
我们用简单的数据渲染的例子来看一下
- 不带key的时候
实例代码如下:
<!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>
</head>
<body>
<div id="app">
<div @click="changeData">切换数据</div>
<li v-for="item in list">{{ item.text }}</li>
</div>
<script src="../../dist/vue.min.js"></script>
<script type="module" crossorigin=“use-credentials”>
var app = new Vue({
el: '#app',
data: {
list: [],
list1: [],
list2: [],
nowDate: '',
updateTime: '',
},
created() {
for (let i = 0; i <= 100000; i++) {
this.list1.push({
id: i,
text: i
})
this.list2.push({
id: i * 2,
text: 100000 - i
})
}
this.list = this.list1
},
methods: {
changeData() {
this.nowDate = Date.now();
this.list = this.list2;
}
},
updated() {
const date = Date.now();
console.log(`updateTime: ${date - this.nowDate}`);
}
})
</script>
</body>
</html>

- 带key的时候
将上述遍历的html代码带上key值。
<li v-for="item in list" :key="item.id">{{ item.text }}</li>

总结: 不带key的速度是更快的。原因有如下:在上述例子中,不带key的省略了销毁和创建dom的开销,只需要替换文本节点就ok了,而带key的却需要进行patch流程,而且需要把能复用的那部分元素找出来,将不能复用的消除,并且重新创建新的dom元素。
不带key会有什么弊病
既然我们从上面检测出来,是不带key的会更快一点,那么我们为什么要带key呢?
1、不带key本身来说没问题,但是当你遍历的元素中,有组件的时候,就会发现问题。因为组件是可以带有自身状态的。我们来看一下下面这个例子:
<!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>
</head>
<body>
<div id="app">
<div v-for="(item, index) in list">
<component-a></component-a>
<button @click="deleteInput(index)">删除</button>
</div>
</div>
<script src="../../dist/vue.min.js"></script>
<script type="module" crossorigin=“use-credentials”>
var ComponentA = {
data: function () {
return {
input: 0
}
},
template: '<input v-model="input" />'
}
var app = new Vue({
el: '#app',
data: {
list: ['1', '2', '3'],
},
methods: {
changeData() {
this.nowDate = Date.now();
this.list = this.list2;
},
deleteInput(index) {
this.list.splice(index, 1);
}
},
components: {
ComponentA
}
})
</script>
</body>
</html>
上述代码很简单,主要实现一下功能:
1、组件携带有自己状态的input
2、页面遍历子组件生成3个input

然后点击中间那个删除,生成的结果:

期望的结果:

大家可以绑key之后试一下,就是我们期望的结果。
2、带key的因为不是就地复用,所以可以实现一些过渡的动画效果。
建议不要用index值去作为key的值
平时我们很喜欢用index去作为key的索引,但是这样的话,key值就失去了本身的意义,在遇到上述那几个例子的时候,同样回去踩坑。
很多集团项目中,会用到eslint去检测template中v-for是否用了key值。从而减少错误率。那么key最好和数据返回的id值去绑定。
总结
这个知识点其实很浅,但是也改变了我的很多认知。
1、不一定很多时候都要写key值,如果是简单的列表渲染,我觉得大可不必。
2、当你要去写key值的时候,最好不要用index,因为index这个值和就地复用没啥区别。
3、当你要实现一些过渡效果的时候,不妨看一下vue的文档,有一些现成的过渡效果可以直接用。