浅谈vue中key的作用及工作原理

641 阅读3分钟

源码中找答案:corn/vdom/patch.js (updateChildren方法)

demo:

const app = new vue({
    el:'#demo',
    data: {item:['a','b','c','d',]},
    mounted(){
        setTimeout(() => {
            this.item.splice(2,0,'E')
        })
    }
})

执行以上代码,正常返回的结果应该是:a,b,e,c,d ,e插入到b和c中间;但是在不适用key的情况回发生什么呢?

由于没有key,程序识别不出来到底要更新谁。所以就只能做一个操作,那就是见着谁就更新谁,这样就会形成一种错误,不改更新的被更新了,需要更新的却没有准确的被更新。

那么再来看下有key的情况

// 第一次循环  patch A
   A B C D
   A B E C D 

// 第二次循环 patch B
    B C D
    B E C D

// 第三次循环    根据首尾判断策略  这次patch D
    C D
    E C D

// 第四次循环   patch C
   C
   E C
// 最后一次新数组只剩下E了,老数组中已经全部patch结束,创建F,插入到C前面

我们现在来看下源码的解释

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {    let oldStartIdx = 0    let newStartIdx = 0    let oldEndIdx = oldCh.length - 1    let oldStartVnode = oldCh[0]    let oldEndVnode = oldCh[oldEndIdx]    let newEndIdx = newCh.length - 1    let newStartVnode = newCh[0]    let newEndVnode = newCh[newEndIdx]    let oldKeyToIdx, idxInOld, vnodeToMove, refElm    // removeOnly is a special flag used only by <transition-group>    // to ensure removed elements stay in correct relative positions    // during leaving transitions    const canMove = !removeOnly    if (process.env.NODE_ENV !== 'production') {      checkDuplicateKeys(newCh)    }    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)) { // 老的开始节点与新的结束节点作比较        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)) { // 老的结束节点与新的开始节点作比较        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)    }  }

function sameVnode (a, b) {  return (    a.key === b.key && (      (        a.tag === b.tag &&        a.isComment === b.isComment &&        isDef(a.data) === isDef(b.data) &&        sameInputType(a, b)      ) || (        isTrue(a.isAsyncPlaceholder) &&        a.asyncFactory === b.asyncFactory &&        isUndef(b.asyncFactory.error)      )    )  )}

假如没有使用key情况,首先我们去daptch  A的时候会走进else if (sameVnode(oldStartVnode, newStartVnode)) { // 老的开头节点与新的开头节点比较

的判断里面,然后执行sameVnode方法,来看下sameVnode方法里面的逻辑;

首先判断新老数据的key是否相同,因为在没有使用key的情况下a.key与b.key都是undefined所以第一层判断为true;然后再来看下tag标签是否相同,毋庸置疑,标签也是相同的。也都不是注释,data也没有发生变化,并且也不是input标签,那么就会认为这两个是相同的节点,就开始打补丁。一次类推,每一步都会走的这一层,就会被强制更新

当有key的情况呢,当patch完B的时候,就会走进else if (sameVnode(oldEndVnode, newEndVnode)) { // 老的末尾节点与新的末尾节点作比较  这层判断,然后以此类推。走进最后一个循环结束的 时候,就已经剩下E了,就会走进这个判断里

if (oldStartIdx > oldEndIdx) {

// 将新的索引值加1,也就是要找到要追加哪个元素的前面还是后面

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)

}

这个时候oldStartIdx是2,oldEndIdx是1,所以进入判断。执行addVnodes方法,进行节点的追加

结论:key的作用主要是高效的更新虚拟dom,从源码层面解释,执行的是daptch.js里面的updateChildren方法,进行节点之间的patch操作,根据判断新老节点使用someVnode方法,首先判断key是否相同,再判断标签,如果没有key的情况,就会认为是相同的节点进行强制更新,如果有key的情况,会根据key是否相同进行更新