AnnVoV / blog

24 stars 2 forks source link

浅析Vue 中的patch和diff(下) #5

Open AnnVoV opened 6 years ago

AnnVoV commented 6 years ago

patch 方法骨架

return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    ...
    if(isUndef(oldVnode)) {
       // empty mount (likely as component), create new root element
      isInitialPatch = true;
      createElm(vnode, insertedVnodeQueue, parentElm, refElm);
    }else{
       // 我们上一次的oldVnode 是Virtual DOM 所以isRealElement为false 
       var isRealElement = isDef(oldVnode.nodeType);
       if(!isRealElement && sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
       }
    }
}

patchVnode 方法

// 比对oldVnode 与 vnode 的方法
function patchVnode(oldVnode, vnode, ...) {
    if (oldVnode === vnode) {
      return
    }
    // elm 中存储的是真实的dom结构,把旧的dom结果先赋值给新vnode
    var elm = vnode.elm = oldVnode.elm;

   ...

    // 如果vnode 节点不是text节点
    if(isUndef(vnode.text)) {
        if(isDef(oldCh) && isDef(ch)) {
            if(oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)            
        }else if(isDef(ch)) {
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        }else if(isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }else if(isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '');
        }
    }else if(oldVnode.text !== vnode.text) {
         // 如果text内容不一样,直接更新
        nodeOps.setTextContent(elm, vnode.text)
    }
}

updateChildren 方法

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0;
    var newStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var 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
    var canMove = !removeOnly;

    {
      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);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        // 注意这里涉及到节点移动
        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);
        // 注意这里涉及到节点移动  
        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);
            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(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

看一下大的结构,分为以下几个维度比较:

总的概括图如下: ​ 前面的4种情况挺容易让人理解的,就是从根节点开始进入patchVnode(oldVnode, vnode),若根节点有children进入updateChildren方法,updateChildren里面定义了新老vnode 树的索引: oldStartIdx, oldEndIdx, newStartIdx, newEndIdx。然后进行4种维度的两两对比。当oldStart与newEnd一致时,会更新oldStart 同时将这个节点移动到oldEnd后面位置; 同理当oldEnd与newStart一致时也会更新并对节点进行移动;如果oldStart 与 newStart一致,直接更新节点内容;如果oldEnd与newEnd一致同理直接更新节点内容。

​ 如果这4种情况都不满足,怎么处理?会进入下面的阶段, 下面这个阶段大部分会进入createElm 这个方法,那什么时候会进入createKeyToOldIdx呢?让我们大致看下这个方法

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

我们来看下createKeyToOldIdx 方法, 大致从这个方法可以看出和:key='xx'这种相关,这个一般在li中vue会给我们建议设置key, 那这个好处到底在于哪里呢?

function createKeyToOldIdx (children, beginIdx, endIdx) {
  var i, key;
  var map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) { map[key] = i; }
  }
  return map
}

举个例子,如果我们不绑key, 如果我们遍历items=[1,2,3,4,5]; 后面更新数据为[1,2,6,3,4,5] 那么dom更新的过程,按照上面的分析必然为下图所示:

而当我们设置了key时,因为进入sameVnode判断的时候会判断key, 所以我们的比较会变成,下图所示,1,2都是同级比较,然后到3的时候会满足oldEnd与newEnd一致,所以开始进入5的比较,依次类推,所以dom都被复用了,最后只要在对应位置插入6就好了

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的作用主要是为了高效的更新虚拟DOM。

参考资料

1.vue2.0 virtual-dom实现简析 https://github.com/DDFE/DDFE-blog/issues/18 2.vue2.0中 v-for的key 到底有什么用? https://www.zhihu.com/question/61064119 3.VirtualDOM与diff(Vue实现) https://github.com/answershuto/learnVue/blob/master/docs/VirtualDOM%E4%B8%8Ediff(Vue%E5%AE%9E%E7%8E%B0).MarkDown