前言
适合已经会使用vue,但从来没去了解key的作用是什么的小伙伴观看。
建议先看【vue回顾系列】03-什么是模板编译,vdom以及它的更新机制是怎么样的,了解diff算法。
key的作用
我们举个例子:
<ul>
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
</ul>
data() {
return {
list: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' }
]
}
}
我们给每个li标签加入index下标作为key值,渲染出来的页面是:
vue的视图渲染过程是,先进行模板编译,结合数据,然后在内存中计算出虚拟DOM,最后挂载到真实页面上。这时我们手动插一条数据{ id: 5, name: 'e' }
到list尾部,那么vue会重新完整的计算一个新的虚拟DOM放到内存中,然后进行新旧虚拟DOM的对比:
// 旧(虚拟dom实际不是长这个样,方便理解就这样写了)
[
<li>a</li>, // key 0
<li>b</li>, // 1
<li>c</li>, // 2
<li>d</li> // 3
]
// 新
[
<li>a</li>, // 0
<li>b</li>, // 1
<li>c</li>, // 2
<li>d</li>, // 3
<li>e</li> // 4
]
vue根据最新虚拟DOM的每个key值去与旧虚拟DOM做对比,先从新虚拟DOM中第一个,也就是key为0开始,是个<li>a</li>
,然后它发现旧的0是个<li>a</li>
,俩哥们长得一模一样,那就不挂载到真实页面上了,减少性能上的开支。以此类推到key为4的时候,vue发现旧虚拟DOM上没有<li>e</li>
,那就需要渲染挂到真实页面上了。
我们从上述过程中可以看到,新增的一条数据,因为有key的帮助,不会使得整个页面全部重新渲染,只渲染出不同的那个。
当页面数据量庞杂且常动态变化的时候,key的作用就能大大提高页面的渲染效率。这个对比的过程也称为diff算法。
key应为唯一标识符
性能损失
如果在一些数据会变动的DOM中,我们最好不要将key写为index索引值,我们来看上面的改造例子,把向后插入数据改为向前插入数据:
<ul>
<button @click="addE">向前添加e</button>
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
</ul>
data() {
return {
list: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' }
]
}
}
methods: {
addE() {
this.list.unshift({ id: 5, name: 'e' })
}
}
当我们点击按钮向前添加一条数据时,我们再来看看新旧虚拟DOM的对比:
// 旧(虚拟dom实际不是长这个样,理解一下就好)
[
<li>a</li>, // key 0
<li>b</li>, // 1
<li>c</li>, // 2
<li>d</li> // 3
]
// 新
[
<li>e</li> // 0
<li>a</li>, // 1
<li>b</li>, // 2
<li>c</li>, // 3
<li>d</li>, // 4
]
你会发现从key为0开始,每个新虚拟DOM都对不上旧的了,这时vue就会认为新虚拟DOM和旧的完全不一致,那么就把新的全部渲染挂载到真实页面上。但其实只有一条数据是新的,浪费了性能。
diff算法导致的显示错误
之前可以看到了,在做新旧虚拟DOM对比的时候,是一个标签一个标签的进行对比,也就是diff算法的最小颗粒度为一个标签。
我们再来点补充,把上面例子的每个li标签中写上一个input标签,并在b标签输入内容后点击添加数据按钮时,会发现:
b的输入框内容跑到a去了,王德发!这是因为:
diff算法的最小颗粒度虽然是一个标签,但是如果标签内部还有标签,那么diff会继续向内比较下去,也就是说可以换掉这一层的标签的同时保持内层标签的不变。
具体可看【vue回顾系列】03-什么是模板编译,vdom以及它的更新机制是怎么样的
以下只是我个人猜想,供参考:
在上面这个例子中,a、b、c、d这一层的节点,因为e的加入a、b、c、d这层DOM节点重新渲染,而他们下面的input层的节点,并不会发生改变,仍然用着key为下标的值去找父节点连接上,所以就发生了连接错位的问题。
key怎么选
一般情况下,如果数组的每个子项中有id属性这样的唯一值,就使用它即可。没有推荐两个办法:
- 第三方轻量库,nanoid。
- 浏览器原生对象
crypto.randomUUID()
,用的时候看看浏览器支持情况
总结
key如果使用index作为值会导致:
- 第一,当进行逆序添加、逆序删除等破坏顺序操作时,会重新渲染,导致性能效率低的问题;
- 第二,容易照成显示错位的问题出现;
如果我们给key指定了唯一的标识符,也就是说给每个数据指定一个身份证一样的东西,那么diff每次就只会通过身份证去认人,就不会出现以上问题,例如用数据中的id字段:
<ul>
<button @click="addE">向前添加e</button>
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
```javascript
// 旧(虚拟dom实际不是长这个样,理解一下就好)
[
<li>a</li>, // key 1
<li>b</li>, // 2
<li>c</li>, // 3
<li>d</li> // 4
]
// 新
[
<li>e</li> // 5
<li>a</li>, // 1
<li>b</li>, // 2
<li>c</li>, // 3
<li>d</li>, // 4
]
这样在新虚拟DOM中key为1-4的都能在旧虚拟DOM中找到对应正确的数据,只会去渲染key为5的,挂载到真实页面上。
当然,如果数据只是展示不操作,那么用index为key也是可以的,不影响。
题外话 v-if一起用的情况
在vue2中,当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。
然而在vue3中反过来了