前言

在vue中使用v-for时,一直有几个疑问:

  • v-for为什么要加key
  • 为什么有时候用index作为key会出错

带着这个疑问,结合各种博客和源码,终于有了点眉目。

virtual dom

要理解diff的过程,先要对virtual dom有个了解,这里简单介绍下。

【作用】

我们都知道重绘和回流,回流会导致dom重新渲染,比较耗性能;而virtual dom就是用一个对象去代替dom对象,当有多次更新dom的动作时,不会立即更新dom,而是将变化保存到一个对象中,最终一次性将改变渲染出来。

【形式】

<div>
<p></p>
<span></span>
</div>

以上代码转换成virtual dom就是如下形式(当然省去了很多其他属性)

{
tag: 'div',
children: [
{
tag: 'p'
},
{
tag: 'span'
}
]
}

diff原理

首先当然是附上这张经典的图

图中很清楚的说明了,diff的比较过程只会在同层级比较,不会跨级比较。

整体的比较过程可以理解为一个层层递进的过程,每一级都是一个vnode数组,当比较其中一个vnode时,若children不一样,就会进入updateChildren函数(其主要入参为newChildren和oldChildren,此时newChildren和oldChildren为同级的vnode数组);然后逐一比较children里的节点,对于children的children,再循环以上步骤。

updateChildren就是diff最核心的算法,源码如下(简要了解就行):

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

diff算法是一个交叉对比的过程,大致可以简要概括为:头头比较、尾尾比较、头尾比较、尾头比较。具体过程可以参见这边博文,里面用例子讲的很清楚。

为什么要加key

可以回头看下上面的源码,有一个sameVnode函数,其源码可以简化为如下:

function sameVnode (a, b) {
return (
a.key === b.key && a.tag === b.tag
)
}

也就是说,判断两个节点是否为同一节点(也就是是否可复用),标准是key相同且tag相同。

以下图的改变(圆圈代表一个vnode,所有node的tag相同)为例

  • 当不加key时,key都是undefined,默认相同,此时就会按照上一节diff算法的就地复用来进行比较:



    以上,B复用A的结构,C复用B的结构,D复用C的结构,E复用D的结构,删除E;然后如果数据有变化,再更新数据

说明:复用是指dom结构复用,如果数据有更新,之后会再进行数据更新

  • 如果加上唯一识别的key



    以上,B、C、D、E全部可以复用,删除A即可

从以上的对比可以看出,加上key可以最大化的利用节点,减少性能消耗

为什么不建议用index作为key

在工作中,发现很多人直接用index作为key,好像几乎没遇到过什么问题。确实,index作为key,在表现上确实几乎没有问题,但它主要有两点不足:

1)index作为key,其实就等于不加key

2)index作为key,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出(这是vue官网的说明)

第一点

对于第一点,还是以上面的例子来说明

虽然加上了key,但key是就是当前的顺序索引值,此时sameNode(A,B)依然是为true,所以与不加key时一样,A复用B的结构并将数据更新为B。下面以一个demo来说明

<div id="demo">
<div v-for="(item,index) in list" :key="index">
<item-box :data=item></item-box>
<button @click="delClick(index)">删除</button>
</div>
</div>
Vue.component('item-box',{
template:'<span>{{data.msg}}</span>',
props: ['data'],
})
var demo = new Vue({
el: '#demo',
data() {
return {
list: [
{
msg: 'k1',
id: 1
},
{
msg: 'k2',
id: 2
},
{
msg: 'k3',
id:3
}
]
}
},
methods: {
delClick (index) {
this.list.splice(index, 1)
}
}
})

操作:删除k1

  • 不加key,或用index作为key,变化过程是

    图a图b图c图d

    也就是



    经过对比,复用1、复用2、删除3(图b),更新1的数据(图c),更新2的数据(图d)

  • 将demo中的key值由index改为item.id,则变化过程是



    也就是

经对比,复用2,复用3,删除1

小结:从以上对比可知,用index做key,与不用key是一样的。由于把源码贴出来比较不易懂,所以只是把debugger源码的结果贴出来了,感兴趣的可以自己去debugger这个过程,理解的会更好。

第二点

第二点有两种情况,我们首先看依赖子组件状态的情况

【依赖子组件状态】

还是刚刚的例子,做一点修改

Vue.component('item-box',{
template:'<button @click="itemClick">{{status}}</button>',
props: ['data'],
data () {
return {
status: 'no'
}
},
methods: {
itemClick () {
this.status = 'yes'
}
}
})

也就是将template里面的数据由props改为data,即子组件内部的数据。

操作:点击第一个no和第二个no,然后点击第一个删除,奇怪的事出现了

  • 不加key,或用index作为key,结果是

本来应该删除第一个的,好像把第三个给删了。是这样么?是的。这个过程相当于第一点里面的图b,但却少了后续数据更新的过程了。为什么不更新数据了呢?因为,数据更新这个步骤是当依赖list的数据发生变化,再根据订阅模式中添加的依赖来依次更新数据(此处可以了解下双向绑定)。可以粗暴的理解为,不依赖于list的数据,此处不关心,不会去更新,流程就停留在图b了,因此我们看到的就是错误的表现了。

  • 将demo中的key值由index改为item.id

此时就是预期的结果啦

小结:以上就是官网里提到的,就地复用的原则不适用于依赖子组件状态的场景,以上例子中,status就是子组件的状态,与外部无关

【依赖临时dom状态】

修改刚刚的demo

<div id="demo">
<div v-for="(item,index) in list" :key="index">
<input type="text">
<button @click="delClick(index)">删除</button>
</div>
</div>

操作:在输入框中分别输入1、2、3,然后删除1

  • 不加key,或用index作为key,结果是

不用多说了,一样的道理,因为这是input的临时状态,与list无关,所以停留在图b的状态就不会继续有数据更新了,我们看到的就是图b的样子了

  • 将demo中的key值由index改为item.id

更不用多说了,这里就是对的了

总结

  • diff算法默认使用“就地复用”的策略,是一个首尾交叉对比的过程。
  • 用index作为key和不加key是一样的,都采用“就地复用”的策略
  • “就地复用”的策略,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
  • 将与元素唯一对应的值作为key,可以最大化利用dom节点,提升性能

参考:

https://www.zhihu.com/question/61064119/answer/183717717

https://www.jianshu.com/p/342e2d587e69

https://codepen.io/vvpvvp/pen/oZKpgE

https://www.jianshu.com/p/cd39cf4bb61d

v-for为什么要加key,能用index作为key么的更多相关文章

  1. Using innodb_large_prefix to avoid ERROR #1071,Specified key was too long; max key length is 1000 bytes

    Using innodb_large_prefix to avoid ERROR 1071        单列索引限制上面有提到单列索引限制767,起因是256×3-1.这个3是字符最大占用空间(ut ...

  2. 数据库六大约束用法:主键(primary key)、外键(foreign key)、非空(not null)、默认(default)、检查(check)、唯一(unique)

    1. 数据库有六大约束 主键(primary key) 外键(foreign key):被参照的键必须有唯一约束或是主键 非空(not null) 默认(default) 检查(check):orac ...

  3. {MySQL完整性约束}一 介绍 二 not null与default 三 unique 四 primary key 五 auto_increment 六 foreign key 七 作业

    MySQL完整性约束 阅读目录 一 介绍 二 not null与default 三 unique 四 primary key 五 auto_increment 六 foreign key 七 作业 一 ...

  4. django.db.utils.OperationalError: (1071, 'Specified key was too long; max key length is 767 bytes')

    环境介绍 Django (2.1)  Python 3.5.5 mysqlclient (1.4.2.post1) Mysql 5.6.28 RHEL 7.3 在migrate时候报错 model代码 ...

  5. 报错:this class is not key value coding-compliant for the key closeLotTextField解决方法

    几种情况下都会报这种错误: 1,加载自定义的tableViewCell的时候总是死在: XInstrumentOpenCell *cell = [tableViewdequeueReusableCel ...

  6. python json格式参数遍历所有key、value 及替换key对于的value

    1.对于接口自动化测试,一般接口以json形式发送返回,往往我们就需要遍历json文件中所有key,value以及修改替换key对于的value. 例如json发送/接收的文件: SendRegist ...

  7. setValue:forUndefinedKey this class is not key value coding-compliant for the key

    下午开发过程中遇到一个错误,结果被的真惨,从上午 11 点查错一直查到下午 2 点才找到错误的原因,真的郁闷的不行. 关于查错这么久,主要的原因是:   1. 自己对 IOS 开发还不熟悉2. 不知道 ...

  8. 数据库操作提示:Specified key was too long; max key length is 767 bytes

    操作重现: 法1:新建连接——>新建数据库——>右键数据库导入脚本——>提示:Specified key was too long; max key length is 767 by ...

  9. Mysql Specified key was too long; max key length is 767 bytes

    今天导入一个数据库时,看到以下报错信息: Specified key was too bytes 直译就是索引键太长,最大为767字节. 查看sql库表文件,发现有一列定义如下: 列   名:cont ...

随机推荐

  1. (转)从0移植uboot(三) _编译最小可用uboot

    ref: https://www.cnblogs.com/xiaojiang1025/p/6436752.html 前两篇介绍了uboot-2013.01的配置原理以及大体的运行流程,本文将讨论如何对 ...

  2. (三十)JSP标签之自定义标签

    创建一个类,引入外部jsp-api.jar包(在tomcat 下lib包里有),这个类继承SimpleTagSupport 重写doTag()方法. jspprojec包下的helloTag类: 1 ...

  3. UnityC#中修改RectTransform

    1.改变RectTransform的Left和Buttom GetComponent<RectTransform>().offsetMax = new Vector2(left, top) ...

  4. 转录组测序(RNA-seq)技术

        转录组是某个物种或者特定细胞类型产生的所有转录本的集合.转录组研究能够从整体水 平研究基因功能以及基因结构,揭示特定生物学过程以及疾病发生过程中的分子机理,已广泛应 用于基础研究.临床诊断和药 ...

  5. Laravel with 查询指定的字段(非复制的哦)

    问题: 在with里面指定查询字段,结果是null. 在模型里面指定查询字段,结果是null. 解决办法: 在查询指定字段的时候要顺带着查询关联的外键,例: // user 表 id name // ...

  6. (详细)JAVA使用JDBC连接MySQL数据库(3)-代码部分

    欢迎任何形式的转载,但请务必注明出处. 本节主要内容 项目建立 数据库连接 数据库操作 主函数 点击进入推荐博客(必看) 一.项目建立 如图所示:新建Java Project.Package.Clas ...

  7. SQL将同样标识的查询结果查重并用逗号拼接

    SELECT B.TaskID , LEFT(SamList, LEN(SamList) - 1) AS ResultListFROM ( SELECT TaskID , ( SELECT Sampl ...

  8. python matplotlib动态绘图

    matplotlib animation的官方文档: http://matplotlib.org/api/animation_api.html 接下来完成一个实时获取cpu数值,并绘图的功能. 1.动 ...

  9. mimikatz记录

    mimikatz需要管理员权限运行 vps监听 nc -lvp 4444 服务器管理员权限执行 mimikatz.exe ""privilege::debug"" ...

  10. 【Zookeeper】应用场景概述

    一.数据发布与订阅(配置中心) 二.负载均衡 三.命名服务(Naming Service) 四.分布式通知/协调 五.集群管理与Master选举 六.分布式锁 七.分布式事务 一.数据发布与订阅(配置 ...