v-for为什么要加key,能用index作为key么

前言

在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算法的就地复用来进行比较:

    以上,A复用B,B复用C,C复用D,D复用E,删除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

原文地址:https://www.cnblogs.com/youhong/p/11327062.html

时间: 2024-08-08 13:21:56

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

xib自定义UIView报错误 &quot;forUndefinedKey:]: this class is not key value coding-compliant for the key&quot;

使用xib自定义UIView的时候, 需要将控件拖拽成属性, 在viewController加载自定义view的时候报错误 "forUndefinedKey:]: this class is not key value coding-compliant for the key" 查找很久,终于发现是xib 的FILE'OWNER 的类关联出错 自定义View的时候  这个位置只能为空!!! xib关联类应该在view的Class设置,如图 修改了类的关联之后,错误解决.

Laravel 5.4 migrate报错:Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `users` add unique `us ers_email_unique`(`email`))

Laravel 5.4 migrate报错:Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `users` add unique `us     ers_email_unique`(`email`)) public function up() { Schema::create('users', function (Blu

&#39;NSUnknownKeyException&#39;, reason:....etValue:forUndefinedKey:]: this class is not key value coding-compliant for the key

erminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<MainTableViewController 0xae26040> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key messageImage1.' 我的是因为lib组件关联IBOutlet的时候关联到了file's ow

iOS 错误&quot;This class is not key value coding-compliant for the key&quot;解决方案

这个错误的出现是因为一个在File's Owner中一个已经链接的Interface Builder对象被删除或重命名了. 在 Interface Builder的Files's Owner右键查看,如果看到有警告标识,就是问题的所在. 下图中"aRemovedView"就有一个警告标识,这是因为我已经将它移除了,但是它还仍链接在IB中.因此就会给出错误:"Terminating app due to uncaught exception 'NSUnknownKeyExcep

Hive集成Mysql作为元数据时,提示错误:Specified key was too long; max key length is 767 bytes

在进行Hive集成Mysql作为元数据过程中,做完所有安装配置工作后,进入到hive模式,执行show databases:执行正常,接着执行show tables:时却报错. 关键错误信息如下: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Specified key was too long; max key length is 767 bytes) 具体操作信息如下: hive> show databases; OK

IOS Exception2 this class is not key value coding-compliant for the key Click

2015-06-16 23:00:53.706 MyIOSPackage[823:280049] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<MyIOSPackage.ViewController 0x7fd760715ac0> setValue:forUndefinedKey:]: this class is not key value coding-compliant f

*** Terminating app due to uncaught exception &#39;NSUnknownKeyException&#39;, reason: &#39;[ViewController &gt; setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ViewController > setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key backBtn.' 第一种情况:xib文件属性输出口连接错误,IBout多连或者忘关输入口 第二种情况:引入第三方的SDK会出现错误,

关于出现this class is not key value coding-compliant for the key的错误

随着应用里的viewcontroller及其中的控件数量越来越多,有的时候报出个莫名其妙的错误都不知道该如何着手去排错,比如我遇到的这个错误: *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<XXXXXXXXXXX 0x000000> setValue:forUndefinedKey:]: this class is not key value coding-compliant

Terminating app due to uncaught exception &#39;NSUnknownKeyException&#39; this class is not key value coding-compliant for the key

 Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ViewController > setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key backBtn.' 第一种情况:xib文件属性输出口连接错误,IBout多连或者忘关输入口 第二种情况:引入第三方的SDK会出现错误,导入的