我第一次接触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会采用就地复用的原则,也就是按顺序来比较节点,比如:
当你插入一条数据时
<ul>
旧节点 新节点
<li>1</li> <li>1</li>
<li>2</li> <li>5</li>
<li>3</li> <li>2</li>
<li>4</li> <li>3</li>
<li>4</li>
</ul>
新节点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'
}
]
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
</li>
</ul>
当你插入一条数据在第一条,得到新旧节点如下:
<ul>
旧节点 新节点
<li key="4">新闻标题4</li>
<li key="1">新闻标题1</li> <li key="1">新闻标题1</li>
<li key="2">新闻标题2</li> <li key="2">新闻标题2</li>
<li key="3">新闻标题3</li> <li key="3">新闻标题3</li>
</ul>
vue根据key去匹配节点,新闻的id是不变的,所以找到旧的对应节点下的内容也是一致,此时,只需要生成 <li>新闻标题4</li>
,并在对应的位置插入即可,这样的dom更新效率更高
在for循环中,可以使用index作为key,但是这会导致数据在插入、删除等破坏列表顺序的操作时,造成效率底下,使用index当key和不加key的效果是一直的,因为当插入一条数据时,节点的key也会跟着改变,还是用新闻列表举例:
当使用index作为key时,新旧节点key的变化和对比关系:
<ul>
旧节点 新节点
<li key="0">新闻标题1</li> <li key="0">新闻标题4</li>
<li key="1">新闻标题2</li> <li key="1">新闻标题1</li>
<li key="2">新闻标题3</li> <li key="2">新闻标题2</li>
<li key="3">新闻标题3</li>
</ul>
你会发现,key相等的节点下的内容几乎都不一致,这就造成了大规模节点的丢弃和重新渲染。
key错误的经典案例
在网上很多讲解key作用的案例中,都会用到<input />
作用案例,大概逻辑如下:
v-for渲染一个列表,使用index作为key,每个列表下都会有一个<input/>
let list = [
{
name: '姓名'
},
{
name: '年龄'
}
]
<div v-for="(item,index) in list" :key="index">
{{item.name}}: <input />
</div>
当列表被插入一个新值时,input并不会跟随原来的item,比如在姓名后面的<input />
输入了内容,当list在第一位插入一个班级时,原本在姓名<input />
的内容变成了班级<input />
的内容了
这是因为<input />
的值在输入框中是临时DOM状态,这个状态跟随的是key,所以才会出现这种情况,但是实际写代码很少会出现这种情况,因为写<input />
一般是为了获取输入,肯定会通过v-model
来绑定值的,一旦绑定了值,<input />
中的值就不是临时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,就可以轻易触发组件的重新创建
<my-component :key="key" :width="width"></my-component>
<button @click="changeWidth">修改组件宽度</button>
export default {
data() {
return {
width: 100,
key: 1
}
},
methods: {
changeWidth() {
this.width += 100
this.key++
}
}
}
这样,在每次修改width时,组件都会重新被创建。
虽然达到了重新执行生命周期的目的,但牺牲了重新创建组件的性能,所以我们在设计组件的时候,应多考虑props被改动的情况(比如用计算属性代替created的执行),才能写出一个受欢迎的组件。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。