vue頁面更新(patch)

patch的流程

組件頁面渲染時,將render返回的新vnode(新節點)和組件實例保存的vnode(舊節點)做爲參數,調用patch方法,更新DOM。

判斷兩個節點是否相同

處理過程當中,須要判斷節點是否相同。相同節點須要知足如下條件:
  • key相同
  • 標籤類型相同
  • 註釋節點標識相同,都是註釋節點,或者都不是註釋節點
  • data的值狀態相同,或者都有值,或者都沒值
function sameVnode (a, b) {// 判斷兩個VNode節點是不是同一個節點
    return (
        a.key === b.key && // key相同
        (
            a.tag === b.tag && // tag相同
            a.isComment === b.isComment && // 註釋節點標識相同
            isDef(a.data) === isDef(b.data) && // data值狀態相同
            sameInputType(a, b) // input的type相同
        )
    )
}

patch方法

patch判斷流程以下:
  • a) 若是新節點爲空,此時舊節點存在(組件銷燬時),調用舊節點destroy生命週期函數
  • b) 若是舊節點爲空,根據新節點建立DOM
  • c) 其餘(若是新舊節點都存在)javascript

    • a) 舊節點不是DOM(組件節點),且新舊節點相同java

      • 執行patchVnode
    • b) 舊節點是DOM元素或者兩個節點不相同node

      • 建立新節點DOM,銷燬舊節點以及DOM。
function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
      return
    }
    ...
    if (isUndef(oldVnode)) {
      isInitialPatch = true;// 組件初始加載
      createElm(vnode, insertedVnodeQueue);
    } else {
      var isRealElement = isDef(oldVnode.nodeType);
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
      } else {
        ...
        var oldElm = oldVnode.elm;
        var parentElm = nodeOps.parentNode(oldElm);// 獲取父元素
        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)// 獲取緊跟的弟弟元素
        );
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0);// 銷燬舊節點以及DOM元素
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode);
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
  }
}

patchVnode方法

當兩個節點相同時,執行patchVnode方法。在處理各類狀況以前,會將舊節點elm屬性值賦值給新節點的elm屬性,保持elm保持一致。

具體流程以下:數組

  • a)若是新舊節點徹底相同(引用相同 oldVnode === vnode)緩存

    • 直接返回不處理
  • b) 若是新節點不是文本節點函數

    • a)都存在子節點,新舊節點的子節點數組引用不一樣(oldCh !== ch)spa

      • updateChildren
    • b)新節點有子節點,舊節點沒有3d

      • 1)查重子節點(key)
      • 2)若是舊節點是文本節點,先清空文本
      • 3)建立子節點DOM元素
    • c)舊節點有子節點,新節點沒有code

      • 移除子節點以及DOM
    • d)舊節點是文本節點blog

      • 清除文本
  • c)若是新節點是文本節點,而且和舊節點文本不相同

    • 則直接替換文本內容。
  • d)其餘(新節點是文本節點,而且和舊節點相同)

    • 不處理
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }
   ...
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch);
        }
        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
        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) {
      nodeOps.setTextContent(elm, vnode.text);
    }
   ...
  }

updateChildren方法

updateChildren方法處理相同新舊節點的子節點。方法定義瞭如下變量(updateChildren的節點都表示的是子節點):
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, // 還沒有處理的舊節點key值映射
        idxInOld, // 與新節點key值相同的舊節點序號
        vnodeToMove, // 與新節點key值相同的舊節點
        refElm;// 指向當前正在處理的新結尾節點的後一個節點(已處理)的DOM元素
根據新舊節點的對比結果,更新DOM元素,此過程並不改變新舊節點的排序。序號指向正在處理的節點,分別是新舊節點的起始和結尾節點。對比過程以新起始節點爲主導,對比方向是由兩側向中間。優先比對新舊節點的起始節點和結尾節點,再查找與新起始節點相同的且未處理的舊節點。當舊節點所有處理完(舊起始和結尾序號重疊),此時新節點可能未處理完,就添加新節點DOM元素。當新節點所有處理完(新起始和結尾序號重疊),可能存在舊節點,就刪除舊節點DOM元素。
具體流程以下:
  1. 新舊子節點的起始序號不大於結尾序號時,執行如下流程:

    • a)若是舊子節點兩側存在undefined節點

      • 舊起始節點undefined,oldStartVnode = oldCh[++oldStartIdx]
      • 舊結尾節點undefined,oldEndVnode = oldCh[--oldEndIdx]
    • b)新舊子節點的起始節點相同(先後比較)

      • patchVNode更新DOM內容
      • oldStartVnode = oldCh[++oldStartIdx]
      • newStartVnode = newCh[++newStartIdx]
    • c)新舊子節點的結尾節點相同(先後比較)

      • patchVNode更新DOM內容
      • oldEndVnode = oldCh[--oldEndIdx]
      • newEndVnode = newCh[--newEndIdx]
    • d)舊起始節點和新結尾節點相同(先後比較)

      • patchVNode更新DOM內容
      • 將舊起始節點DOM添加到舊結尾節點DOM前面
      • oldStartVnode = oldCh[++oldStartIdx]
      • newEndVnode = newCh[--newEndIdx]
    • e)舊結尾節點和新起始節點相同(先後比較)

      • patchVNode更新DOM內容
      • 將舊結尾節點DOM添加到舊起始節點DOM前面
      • oldEndVnode = oldCh[--oldEndIdx]
      • newStartVnode = newCh[++newStartIdx]
    • f)其餘(緩存還沒有處理的舊節點key值,依此判斷舊節點中是否存在和新起始節點相同的節點)

      • a)還沒有處理的舊節點中不存在與新起始節點相同的節點

        • 建立新節點DOM並添加到舊起始節點DOM的前面
        • newStartVnode = newCh[++newStartIdx]
      • b)舊節點中存在與新起始節點key相同的節點

        • a)舊節點中存在與新起始節點相同的節點

          • patchVode
          • 將相同的舊節點DOM添加到舊起始節點DOM前面
          • 將相同的舊節點置爲undefinedoldCh[idxInOld] = undefined
          • newStartVnode = newCh[++newStartIdx]
        • b)key相同,但標籤類型不一樣的節點

          • 建立新節點DOM並添加到舊起始節點DOM的前面
          • newStartVnode = newCh[++newStartIdx]
  2. 循環結束

    • a)若是舊節點遍歷完(oldStartIdx > oldEndIdx

      • 把剩餘未處理新節點DOM添加到上一個新結尾節點DOM前面(重新起始節點到新結尾節點,都未處理過)
    • b)若是新節點遍歷完(newStartIdx > newEndIdx

      • 移除舊起始和結尾節點以及他們之間的節點的DOM(從舊起始節點到舊結尾節點,可能存在處理過的節點,但處理過已被置爲undefined)
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;
   ...
    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); }// 緩存還沒有處理的舊節點key值
        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(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

updateChildren的示例:

1.左邊表示新舊節點,節點下面標識起始和結尾節點(即正在處理的節點)。右邊表示當前的DOM。
updateChildren1.jpg

2.新節點的起始和結尾節點與舊節點的起始和結尾節點互不相同,而且在舊節點中未找到與新起始節點(新節點f)相同的節點。
因此建立節點f的DOM並添加到舊起始節點(舊節點a)DOM的前面,而後新起始節點序號加1,表示新節點f已處理,當前正在處理新起始節點c
updateChildren2.jpg

3.新節點的起始和結尾節點與舊節點的起始和結尾節點互不相同,但在舊節點中找到與新起始節點(節點c)相同的節點。
因此將舊節點c的DOM添加到舊起始節點(舊節點a)DOM的前面,舊節點c置空,而後新起始節點序號加1,表示新節點c已處理,當前正在處理新起始節點e

updateChildren3.jpg

4.新起始節點(新節點e)和舊結尾節點(舊節點e)相同。更新舊節點e的DOM內容,並將舊節點e的DOM移動到舊起始節點(舊節點a)DOM的前面,舊結尾節點序號減1,新起始節點加1,表示新舊節點e已處理,當前正在處理的是新起始節點g和舊結尾節點d

updateChildren4.jpg

5.新結尾節點(新節點d)和舊結尾節點(舊節點d)相同。僅更新舊節點d的DOM內容。新結尾節點序號減1,舊結尾節點序號減1,表示新舊節點d已處理,當前正在處理的是新結尾節點g和舊結尾節點c。因爲舊節點c爲空,則舊結尾節點爲b

updateChildren5.jpg

6.新節點的起始和結尾節點與舊節點的起始和結尾節點互不相同,而且在舊節點中未找到與新起始節點(新節點g)相同的節點。
因此建立節點g的DOM並添加到舊起始節點(舊節點a)DOM的前面,而後新起始節點序號加1,表示新節點g已處理,當前正在處理新起始節點d

updateChildren6.jpg

7.因爲新起始和結尾節點序號重疊,新節點已經處理完畢,存在還沒有處理的舊節點,則移除未處理的舊節點DOM。

updateChildren7.jpg

8.結束,最終的DOM。
updateChildren8.jpg

相關文章
相關標籤/搜索