我第一次接触key,是在学习v-for循环时,不加key标识会报错,对于初学者来说,听过key的原理也会懵懵懂懂,一知半解。
当我用了很久的vue,在回头看key的原理时,才彻底弄懂了key的原理和作用。
虚拟Dom渲染
要想了解key的原理,绕不开的就是虚拟dom的渲染过程,因为key最大的作用就是标识节点,以便相同的节点可以被高效复用。
在vue渲染过程中,vue会先生成虚拟dom,也就是用JavaScript中的对象完整描述真实的dom节点,生成的虚拟dom类似如下:
[
{
tagName: 'div',
children: [
{
tagName: 'p',
props: {
class: 'row'
}
// ...
}
],
props: {
id: 'app'
}
// ...
}
]
生成虚拟dom后,再通过虚拟dom渲染出真实节点,页面便有了,此时页面存在真实dom节点和虚拟dom节点两部分。
当你修改了dom节点后,虚拟dom会重新生成,此时页面有新旧两个虚拟dom树,vue会比较两个虚拟dom对象,把完全相同的虚拟dom对应的真实dom节点直接复用,不同的部分进行真实dom更新,这个比较的过程就是所谓的diff算法,这样处理使得真实的dom的更新最小化,从而使得性能更优。
key的用处
在比较虚拟dom的过程中,如果没有key,vue会采用就地复用的原则,也就是按顺序来比较节点,比如:
当你插入一条数据时
旧节点 新节点
- 1
- 1
- 2
- 5
- 3
- 2
- 4
- 3
- 4
新节点li-1
和旧节点li-1
比较,完全一致,不生成新dom节点,直接复用旧dom节点;
新节点li-5
和旧节点li-2
比较,li一样,内容不一样,复用li节点,文本节点5
重新生成并替换旧的文本节点2
...
新节点li-4
找不到旧的节点对比,直接创建新的dom节点
以此类推,如果li里不是简单的数字,而是其他复杂的组件,那么操作dom的成本就更高了。
如何让vue在diff时知道新节点和哪些旧节点去比较呢?那就是key起的作用,如果给节点绑定了key属性,那vue在diff的时候,会找到与新节点的key相同的旧节点进行比较,比如我们渲染了一页新闻列表:
let list = [
{
id: 1,
title: '新闻标题1'
},
{
id: 2,
title: '新闻标题2'
},
{
id: 3,
title: '新闻标题3'
}
]
-
{{item.title}}
当你插入一条数据在第一条,得到新旧节点如下:
旧节点 新节点
- 新闻标题4
- 新闻标题1
- 新闻标题1
- 新闻标题2
- 新闻标题2
- 新闻标题3
- 新闻标题3
vue根据key去匹配节点,新闻的id是不变的,所以找到旧的对应节点下的内容也是一致,此时,只需要生成
,并在对应的位置插入即可,这样的dom更新效率更高
在for循环中,可以使用index作为key,但是这会导致数据在插入、删除等破坏列表顺序的操作时,造成效率底下,使用index当key和不加key的效果是一直的,因为当插入一条数据时,节点的key也会跟着改变,还是用新闻列表举例:
当使用index作为key时,新旧节点key的变化和对比关系:
旧节点 新节点
- 新闻标题1
- 新闻标题4
- 新闻标题2
- 新闻标题1
- 新闻标题3
- 新闻标题2
- 新闻标题3
你会发现,key相等的节点下的内容几乎都不一致,这就造成了大规模节点的丢弃和重新渲染。
key错误的经典案例
在网上很多讲解key作用的案例中,都会用到作用案例,大概逻辑如下:
v-for渲染一个列表,使用index作为key,每个列表下都会有一个
let list = [
{
name: '姓名'
},
{
name: '年龄'
}
]
{{item.name}}:
当列表被插入一个新值时,input并不会跟随原来的item,比如在姓名后面的输入了内容,当list在第一位插入一个班级时,原本在姓名
的内容变成了班级
的内容了
这是因为的值在输入框中是临时DOM状态,这个状态跟随的是key,所以才会出现这种情况,但是实际写代码很少会出现这种情况,因为写
一般是为了获取输入,肯定会通过
v-model
来绑定值的,一旦绑定了值,中的值就不是临时DOM状态,那就不会出现上面那种情况了。
key的其他用途 - 更新key实现组件重新渲染
key有个不太正规,但某些情况非常好用的功能,比如有些组件,在创建的时候有初始化的行为:
export default {
// ...
data() {
return {
_width: 0
}
},
props: {
width: {
type: [Number,String]
}
},
created() {
if (typeof this.width === 'number') {
this._widht = this.width + 'px'
} else {
this._widht = this.width
}
}
}
上述的代码是一个简单的列子,宽度可以传数字或字符串类型,如果传的是数字类型,创建组件的时候就拼接上px(该例子可以使用计算属性完成,此处为了展示)。
类似的组件,在props修改的时候,_width并不会被更新,除非监听width属性。
如果组件的生命周期里还写了很多依赖props的逻辑代码,那不如让组件重新创建好了。
利用key,就可以轻易触发组件的重新创建
export default {
data() {
return {
width: 100,
key: 1
}
},
methods: {
changeWidth() {
this.width += 100
this.key++
}
}
}
这样,在每次修改width时,组件都会重新被创建。
虽然达到了重新执行生命周期的目的,但牺牲了重新创建组件的性能,所以我们在设计组件的时候,应多考虑props被改动的情况(比如用计算属性代替created的执行),才能写出一个受欢迎的组件。