前言
本人在工作中一开始只负责前端,到后来用nodejs写服务以及负责一些团队基础设施等运维工作。由于做的事情太杂,最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。
v-for的渲染逻辑
我们在用v-for指令时,假如没有给每一项加入属性key。页面还是可以正常渲染的。那么究竟为什么还要用key呢?先来看官方文档的说明。
简单来说,就是vue在处理v-for的内容时,每次渲染并不会把内容删除,替换。而是尽可能的利用原有的dom,只替换dom中的内容。举个例子:
//原来的data
[
{text:1},
{text:2},
{text:3}
]
//执行Array.reverse()
//新的data为
[
{text:3},
{text:2},
{text:1}
]
执行reverse后,原数组已经被修改了。也就是第一项与第三项互换位置了。如果按同等逻辑dom中应该也要1,3项互换。但这样显然不是最优的渲染方案,vue实现的效果是,不改变dom的位置,只把1,3项的text内容互换了。
为什么要有key?
既然vue选择了这种渲染逻辑,那就必须要解决一个问题。怎么记录dom与data之间的关系?我们之前在手写MVVM框架文中提过vue的渲染思路,其中有一个方法sameVnode用作判断vNode是是不是同一个。而方法中注意是用tag和key是否一致来决定的,这里的key。就是在v-for中,我们需要写的key。
有了key之后,vNode可以实现与data改变位置相同的效果。
// 假如原来渲染的vNode如下
[
{
tag:'li',
text:1
},
{
tag:'li',
text:2
},
{
tag:'li',
text:3
},
]
// 位置互换之后,并不能判断是位置改变了,还是只是单纯的修改了text
[
{
tag:'li',
text:3
},
{
tag:'li',
text:2
},
{
tag:'li',
text:1
}
]
// 加入ke之后渲染的vNode应该如下
[
{
tag:'li',
text:1,
key:0
},
{
tag:'li',
text:2,
key:1
},
{
tag:'li',
text:3,
key:2
},
]
// 可以根据key知道数据位置改变了
[
{
tag:'li',
text:3,
key:2
},
{
tag:'li',
text:2,
key:1
},
{
tag:'li',
text:1,
key:0
}
]
为什么要避免用index作key?
我们在用写业务时,很多朋友都会习惯用index作为,原因一是方便,不确定数据中是否有唯一id。二是纯粹为了避免编辑器的错误提示。
但我们来仔细想一想,index可以作为v-for的key吗?
//假设我们的data仍然是
data(){
return {
list:[
{text:1},
{text:2},
{text:3}
]
}
}
//template中
<ul ref="ul">
<li v-for="(item,index) of list" :key='index'>{{item.value}}</li>
</ul>
// 首次渲染出来的vNode大致为
[
{
tag:'li',
text:1,
key:0
},
{
tag:'li',
text:2,
key:1
},
{
tag:'li',
text:3,
key:2
},
]
// data执行reverse之后,由于key在首次渲染时,已经定了是index的顺序,导致key没有变但text改变了。
[
{
tag:'li',
text:3, // 被修改
key:0
},
{
tag:'li',
text:2,
key:1
},
{
tag:'li',
text:1, // 被修改
key:2
}
]
可以看到上述例子,由于key的生成是在首次渲染时候完成的(当然你可以后来修改,但这里先不讨论)。我们在执行reverse之后,key实际上是没有被改变的,变的是text。根据Vue的渲染机制,这个text被修改之后。触发子组件视图的重新渲染等一套很重的逻辑。原本我们期待的是dom只要简单把text互换就可以了,可现在缺导致了一系列的子组件渲染成本。
为什么要避免修改使用index作key的数组长度?
同理当我们用index作为v-for的key之后,如果后续修改了数据的长度,同样会造成期待以外的结果。
//如果执行shift把data的第一项去掉,新的data应该是
data(){
return {
list:[
{text:2},
{text:3}
]
}
}
//新的vNode却是
[
{
tag:'li',
text:2,
key:0
},
{
tag:'li',
text:3,
key:1
}
]
vue发现新的vNode少了第三项,就会把key为2(即第三项)删除。但我们本身期待的是把第一项删除,因此会出现错误结果。
为什么不能用随机数作key?
假如用随机数作为key,写法如下:
<ul ref="ul">
<li v-for="(item,index) of list" :key='Math.random()'>{{item.value}}</li>
</ul>
这里会触发一个误区,就是每次template 的渲染,都会执行一遍Math.random(),也就是说,每次生成新vNode的时候,key都是不一样的。我们从官方文档中可以看到。
当key被修改之后,渲染是会重新进行的,这也意味着沉重的渲染成本。
可能会出现的错误结果
这里举出2个由于使用了index作为key之后可能会出现的错误结果。
把index当作参数传入方法。
<ul ref="ul">
<li v-for="(item,index) of list" :key='index' @click="clickBtn(index)">{{item.value}}</li>
</ul>
methods: {
clickBtn(index){
console.log(index)
}
},
这种情况,一开始我们点击第一项的时候,打印出来的结果是跟预期一样的。但如果执行了reverse(),即把1,3互换之后。发现点击1打印的是3,点击3打印的是1.
在dom上加入的事件不会跟着数据顺序变化
如果我们在只能项的dom上加入了事件
<ul ref="ul">
<li v-for="(item,index) of list" :key='index' @click="clickBtn(index)">{{item.value}}</li>
</ul>
//或者在组件生命周期中加入事件。
这样一开始只有点击第一项的时候,会触发事件。但是执行reverse()之后。我们期待的是只有第三项可以触发事件。可是实际上,事件还是在text为3的第一项中生效。
总结
v-for是很实用的vue指令,其实只要是正确的使用都不会出现太多的问题。用index 作key是很常见的业务代码,短期内,开发者可能会没有发现有什么问题,程序可以正常运行。但是作为对技术,产品有追求的开发者,希望大家可以了解到这种方式的弊端。之后尽可能的避免这种用法。