vue中key属性的作用,为什么还是爱
在Vue中使用虚拟dom,根据diff算法将新dom与旧DOM进行比较,从而更新真实DOM。key是虚拟DOM对象的惟一标识符,它在diff算法中起着极其重要的作用。解释了key的作用,以及为什么最好不要用index作为key的属性值。
目录
序言关键字在diff算法中的角色角色同步头节点同步尾节点添加新节点删除冗余节点最长增量子序列为什么不使用索引性能消耗数据错位解决方案摘要
前言
在前端开发中,每当涉及到列表渲染的时候,无论是React还是Vue framework都会提示或者要求每个列表项使用唯一的键,所以很多开发人员在不了解键的原理的情况下,会直接使用数组的索引作为键的值。然后本文会解释key的作用,以及为什么最好不要用index作为key的属性值。
key 的作用
在Vue中使用虚拟dom,根据diff算法将新dom与旧DOM进行比较,从而更新真实DOM。key是虚拟DOM对象的惟一标识符,它在diff算法中起着极其重要的作用。
key 在 diff 算法中的角色
其实React和Vue中的diff算法大致相同,只是diff的比较方法有很大不同,甚至每个版本的diff都有很大不同。下面我们以Vue3.0 diff算法为切入点,分析一下key在diff算法中的作用。
具体的diff过程如下
在Vue3.0中,patchChildren方法中有这样一个源代码。
if(修补标志0) {
if (patchFlag PatchFlags。键控_片段){
/*用于关键字情况下的diff算法*/
patchKeyedChildren(
.
)
返回
} else if (patchFlag PatchFlags。unkeed _ FRAGMENT){
/*对于没有键的情况,直接打补丁*/
patchUnkeyedChildren(
.
)
返回
}
}
PatchChildren根据键的存在执行真正的diff或直接修补。key不存在的情况我们就不做深入研究了。
我们先来看看一些声明的变量。
/* c1旧虚拟节点c2新虚拟节点*/
Let=0/*记录索引*/
Const l2=c2.length /*新vnode的数量*/
E1=c1.length-1/*旧vnode的最后一个节点的索引*/
E2=L2-1/*新节点的最后一个节点的索引*/
同步头部节点
第一步,从头开始寻找同一个vnode,然后打补丁。如果不是同一个节点,那么立即跳出循环。
//(a b) c
//(a b) d e
/*从头开始通过比较找到具有相同节点的补丁。如果不一样,马上跳出来*/
while (i=e1 i=e2) {
常数n1=c1[i]
const n2=(c2[i]=优化
?cloneIfMounted(c2[i]作为VNode)
:normalizeVNode(c2[i]))
/*确定键和类型是否相等*/
if (isSameVNodeType(n1,n2)) {
补丁(
.
)
}否则{
破裂
}
我
}
流程如下:
isSameVNodeType的作用是判断当前的vnode类型和vnode的key是否相等。
导出函数isSameVNodeType(n1: VNode,n2: VNode): boolean {
返回n1 . type===N2 . type n1 . key===N2 . key
}
其实看到这里,我们已经知道了key在diff算法中的作用,就是判断是否是同一个节点。
同步尾部节点
第二步与上一步相同,不同的是从最后开始。
//a (b c)
//d e(公元前)
/*如果第一步没有补丁,立即从后向前开始打补丁。如有不同,立即跳出循环*/
while (i=e1 i=e2) {
常数n1=c1[e1]
常数n2=(c2[e2]=优化
?cloneIfMounted(c2[e2]作为VNode)
:normalizeVNode(c2[e2]))
if (isSameVNodeType(n1,n2)) {
补丁(
.
)
}否则{
破裂
}
e1 -
e2 -
}
第一步之后,如果发现没有补片完成,那么立即进行第二步,从尾部开始遍历前向diff。如果没有找到相同的节点,则立即跳出循环。流程如下:
添加新的节点
步骤3:如果所有旧节点都打了补丁,而新节点没有打补丁,则创建一个新的vnode。
//(a b)
//(a b) c
//i=2,e1=1,e2=2
//(a b)
//c (a b)
//i=0,e1=-1,e2=0
/*如果新节点的数量大于旧节点的数量,所有剩余的节点将被新的vnode处理(这种情况表明同一个VNODE已被修补)*/
如果(i e1) {
if (i=e2) {
const nextPos=e2 1
const anchor=nextPos l2?(c2[nextPos]作为VNode)。埃尔:父母锚
while (i=e2) {
补丁(/*创建新节点*/
.
)
我
}
}
}
流程如下:
删除多余节点
第四步:如果所有的新节点都打了补丁,剩下旧节点,卸载所有的旧节点。
//i e2
//(a b) c
//(a b)
//i=2,e1=2,e2=1
//a (b c)
//(公元前)
//i=0,e1=0,e2=-1
else if (i e2) {
while (i=e1) {
unmount(c1[i],parentComponent,parent悬念,true)
我
}
}
流程如下:
最长递增子序列
此时,核心场景还没出现。运气好的话可能到这里就结束了,不能完全靠运气。剩下的场景是新旧节点都有多个子节点的情况。那我们来看看Vue3是怎么做出来的。为了组合移动、添加和卸载操作
每移动一个元素,就能找到一个规律。如果我们想最少移动它,就意味着某些元素需要稳定。那么能保持稳定的元素规则是什么呢?
看一下上面的例子:c h d e VS d e i c对比的时候,肉眼可以看到,只需要把C移到最后,然后卸载H,加上I E就可以保持不变,可以发现d e在新旧节点的顺序不变,D在E后面,下标是递增的。
这里我们引入一个叫做最长增长子序列的概念。
官方解释:在给定的数组中,找一组递增的值,长度尽可能大。
有点难以理解,我们来看具体例子:
const arr=[10,9,2,5,3,7,101,18]
=[2, 3, 7, 18]
这个数组是arr最长的递增子序列,也是[2,3,7,101]。
因此,最长的递增子序列满足三个要求:
1.子序列中的值是递增的。
2.子序列中值的下标在原始数组中是递增的。
3.这个子序列是能找到的最长的一个。
但我们通常会找到值较小的系列,因为它们有更大的增长空间。
接下来的思路是:如果能在新的节点序列中找到旧节点顺序相同的节点,就知道哪些节点不需要移动,然后只需要插入不在这里的节点。因为最后要呈现出来的顺序是新节点的顺序,移动是只要老节点移动,所以只要老节点保持最长顺序不变,通过移动个别节点,就能够跟它保持一致。所以在这之前,先找到所有的节点,再找到对应的序列。其实我们最不需要的就是这个数组:[2,3,new,0]。其实这就是diff mobile的思路。
为什么不要用 index
性能消耗
当使用index作为键时,顺序操作被打破,因为每个节点都找不到对应的键,有些节点无法重用,所有新的vnode都需要重新创建。
示例:
模板
你好
保险商实验所
li v-for=(item,index)in student list :key= index { { item . name } }/Li
英国铁路公司
Button @click=addStudent 添加一条数据/按钮
/ul
/div
/模板
脚本
导出默认值{
名称:“HelloWorld”,
data() {
返回{
学生列表:[
{id: 1,姓名:张三,年龄:18},
{id: 2,姓名:李四,年龄:19},
],
};
},
方法:{
addStudent(){
Const studentObj={id: 3,姓名:王五,年龄:20 };
this.studentList=[studentObj,this.studentList]
}
}
}
/脚本
我们先打开Chorme调试器,双击修改里面的文本。
让我们运行上面的代码,看看运行结果。
从上面的运行结果可以看出,我们只是添加了一条数据,但是这三条数据都需要重新渲染,是不是很神奇?我明明只是插入了一段数据,为什么三段数据都要重新渲染?而我想要的只是新添加的要重新渲染的数据。
上面我们也讲了diff比较法。我们根据diff对比来画个图,看看是怎么对比的。
当我们在前面添加一条数据时,索引序列会被打断,这会导致所有新的节点键发生变化,这样我们页面上的所有数据都会被重新渲染。
下面我们生成1000个DOM,比较一下使用index和不使用index的性能。为了保证密钥的唯一性,我们使用uuid作为密钥。
我们先用index作为键来执行。
模板
你好
保险商实验所
Button @click=addStudent 添加一条数据/按钮
英国铁路公司
li v-for=(item,index)in student list :key= index { { item . id } }/Li
/ul
/div
/模板
脚本
从“uuid/v1”导入uuidv1
导出默认值{
名称:“HelloWorld”,
data() {
返回{
学生列表:[{id:uuidv1()}],
};
},
已创建(){
for(设I=0;i 1000i ) {
this.studentList.push({
id: uuidv1(),
});
}
},
更新之前(){
console.time(for )。
},
已更新(){
console . time end( for )//for:75.2203125毫秒
},
方法:{
addStudent(){
const studentObj={ id:uuid v1()};
this.studentList=[studentObj,this.studentList]
}
}
}
/脚本
使用id作为密钥。
模板
你好
保险商实验所
Button @click=addStudent 添加一条数据/按钮
英国铁路公司
li v-for=(item,index)in student list :key= item . id { { item . id } }/Li
/ul
/div
/模板
更新之前(){
console.time(for )。
},
已更新(){
console . time end( for )//for:42.434373736376
},
从上面的比较可以看出,使用唯一值作为键可以节省开销。
数据错位
上面的例子可能会认为使用索引作为键只是影响页面加载的效率,少量的数据影响不大。那么在下面这种情况下,index可能会出现一些意想不到的问题,或者上面的场景。这时候我会在每个文字内容后面加一个输入输入框,在输入框里手动填写一些内容,再加一个同学看穿按钮。
模板
你好
保险商实验所
li v-for=(item,index)in student list :key= index { { item . name } } input//Li
英国铁路公司
Button @click=addStudent 添加一条数据/按钮
/ul
/div
/模板
脚本
导出默认值{
名称:“HelloWorld”,
data() {
返回{
学生列表:[
{id: 1,姓名:张三,年龄:18},
{id: 2,姓名:李四,年龄:19},
],
};
},
方法:{
addStudent(){
Const studentObj={id: 3,姓名:王五,年龄:20 };
this.studentList=[studentObj,this.studentList]
}
}
}
/脚本
让我们在input中输入一些值,并添加一个同学来看看效果:
这个时候我们会发现,添加之前输入的数据放错了位置。加了王五之后,张三的信息还留在输入框里,显然不是我们想要的结果。
从上面的比较可以看出,当使用index作为键时,比较时发现虽然文本值发生了变化,但继续向下比较时,发现输入的DOM节点和原来的是一样的,于是重用。但是没想到输入的输入框还是输入值,然后输入值就放错了。
解决方案
既然我们知道index的使用在某些情况下有很不好的影响,那么在开发中如何解决这种情况呢?其实只要密钥不变,一般用在以下三种情况。
在开发中,每一条数据最好使用固定唯一标识的数据作为key,比如ID、手机号、身份证号等后台返回的唯一值。
你可以用符号作为钥匙。Symbol是ES6,它引入了一个新的原始数据类型Symbol来表示唯一值。最大的用途是定义一个对象的唯一属性名。
A=符号(“测试”)
让b=符号(“测试”)
console.log(a===b)//false
Uuid可以用作密钥。uuid是通用唯一标识符(Universally Unique Identifier)的缩写,是机器在一定范围内(从特定命名空间到全球)唯一生成的标识符。
我们以上面的第一个方案为重点,再来看看上面的情况,如图。键,相同的节点被多路复用。它在diff算法中起着真正的作用。
总结
当索引作为键时,数据反序添加和反序删除会产生不必要的真实DOM更新,效率低下。
当使用index作为键时,如果结构包含输入类的DOM,将会生成错误的DOM更新。
在开发中,每一条数据最好使用固定唯一标识的数据作为key,比如ID、手机号、身份证号等后台返回的唯一值。
如果没有逆序添加、删除数据等破坏顺序的操作,可以只在渲染和显示时使用index作为键(但还是不建议使用,以便养成良好的开发习惯)。
关于为什么不建议在Vue中使用index作为key的这篇文章到此为止。请搜索我们以前的文章或继续浏览下面的相关文章,了解更多关于Vue index作为键的信息。希望你以后能支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。