Vue源码解析问题(3 key的作用与原理)

345 阅读3分钟

3. vue中key的作用和工作的机制都是什么?

创建demo

<button @click="set">set</button>
<p v-for="child in children">{{child}}</p><!-- 不使用key -->
<p v-for="child in children" :key="child">{{child}}</p><!-- 使用key -->
----------------------------------------------------------------------
js  data声明
children: ['a', 'b', 'c', 'd', 'e']
set () { this.children.splice(3, 0, 'f') }
// set 后得到数组['a', 'b', 'c', 'f', 'd', 'e']

上述代码中,在数组children 第四项 d 前插入一项 f, 执行 加key 与 不加key 后比较区别

源码位置

url src\core\vdom\patch.js => updateChildren => sameVnode

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]
  }
}

不使用key 代码执行如下

// 第1次循环patch  (因为 a.key = a.key 并无渲染执行 不做处理)
oldList:  a b c d e
newList:  a b c f d e

// 第2次循环patch  (同1)
oldList:  b c d e
newList:  b c f d e

// 第3次循环patch  (同1)
oldList:  c d e
newList:  c f d e

// 第4次循环patch  (虽然 d.key = f.key 但 内容发生改变 做重新渲染)
oldList:  d e
newList:  f d e

// 第5次循环patch  (同4)
oldList:  e
newList:  d e

// 第6次循环patch  (此时新数组多出一项,做创建dom操作)
oldList:  
newList:  e

结论:由此可见,当不设置key时,会按照自上而下执行,遇到不同的位置,会做重新渲染操作。删除同理,先会重新渲染,然后销毁dom,极其浪费性能。

使用key 代码执行如下

// 第1次循环patch  (因为 a.key = a.key 并无渲染执行 不做处理)
oldList:  a b c d e
newList:  a b c f d e

// 第2次循环patch  (同1)
oldList:  b c d e
newList:  b c f d e

// 第3次循环patch  (同1)
oldList:  c d e
newList:  c f d e

// 第4次循环patch  (此时 d.key !== f.key 会从末项比较)
oldList:  d e
newList:  f d e

// 第5次循环patch  (同4)
oldList:  d
newList:  f d

// 第6次循环patch  (old全部处理结束,new中剩下的f, 创建f并插入在d前面)
oldList:  
newList:  f

结论:设置key后,会先从首项对比,如遇到不同项,会从末项对比,避免频繁更新不同元素,减少dom操作。nice

总结

  1. key的作用主要是为了高效的更新虚拟DOM,原理是vue在patch 过程中通过key 可以精准判断两个节点是否相同,从而避免频繁更新不同元素,使整个patch过程更高效,减少DOM操作量,提高性能。
  2. 如果不设置key,会导致不同的bug,例如使用相同标签名的元素过渡切换时,会用到key 用于vue做区分,不设置会导致vue只会替换其内部属性不会触发过渡效果