走進snabbdom—Vue2背後的Virtual-DOM的機制

Dominic_Ming發表於2019-03-03

snabbdom 是什麼

snabbdom是一個Virtual-DOM的實現庫,它專注於使用的簡單以及功能和的模型化,並在效率和效能上有著很好的表現。如果你還不知道什麼是Virtual-DOM技術,它是一種網頁中通過diff演算法來實現網頁修改最小化的方法,react底層使用了這樣的機制來提高效能。

從Vue2釋出開始,也開始使用了這樣的機制。Vue並沒有選擇自己重新造一套Virtual-DOM的演算法,而是在snabbdom的基礎上構建了一個嵌入了框架本身的fork版本。可以說,Vue就是在使用snabbdom的Virtual-DOM演算法。

snabbdom 的特性

  • snabbdom核心演算法就兩三百多行,閱讀和理解都是非常方便的。
  • module劃分清楚,擴充性強
  • 自帶一系列hook,這些hook可以在diff演算法的各處呼叫,可以使用hook定製過程
  • 在Virtual-DOM眾多演算法中有著優秀的效能
  • 函式都帶有和自己簽名相關的reduce/scan函式,方便函式響應式程式設計使用
  • h函式可以簡單的建立vnode節點
  • 對於SVG,使用h函式可以輕鬆加上名稱空間

snabbdom核心概念

  • init

    snabbdom使用一種類似於外掛宣告使用的方式來模組化功能,如果你使用過AngularJS的宣告注入或者Vue.use,你對這樣的方式一定不陌生。

    var patch = snabbdom.init([
      require(`snabbdom/modules/class`).default,
      require(`snabbdom/modules/style`).default,
    ]);
    複製程式碼
  • patch

    patch是由init返回的一個函式,第一個引數代表著之前的view,是一個vnode或者DOM節點,而第二個引數是一個新的vnode節點,oldNode會根據他的型別被相應的更新。

    patch(oldVnode, newVnode);
    複製程式碼
  • h函式

    h函式可以讓你更加輕鬆的建立vnode。

    var snabbdom = require(`snabbdom`)
    var patch = snabbdom.init([ // 呼叫init生成patch
      require(`snabbdom/modules/class`).default, // 讓toggle class更加簡單
      require(`snabbdom/modules/props`).default, // 讓DOM可以設定props
      require(`snabbdom/modules/style`).default, // 支援帶有style的元素,以及動畫
      require(`snabbdom/modules/eventlisteners`).default, // 加上事件監聽
    ]);
    var h = require(`snabbdom/h`).default; // h的意思是helper,幫助建立vnode
    var toVNode = require(`snabbdom/tovnode`).default;
    
    var newNode = h(`div`, {style: {color: `#000`}}, [
      h(`h1`, `Headline`),
      h(`p`, `A paragraph`),
    ]);
    
    patch(toVNode(document.querySelector(`.container`)), newVNode)
    複製程式碼
  • 鉤子(hook)

    名稱 觸發時間 回撥引數
    pre patch開始 none
    init vnode被新增的時候 vnode
    create DOM元素被從create建立 emptyVnode, vnode
    insert 一個元素被插入了DOM vnode
    prepatch 元素即將被patch oldVnode, vnode
    update 元素被更新 oldVnode, vnode
    postpatch 元素被patch後 oldVnode, vnode
    destroy 元素被直接或者間接移除 vnode
    remove 元素直接從DOM被移除 vnode, removeCallback
    post patch操作結束 none

snabbdom 演算法

diff兩棵樹的演算法是一個O(n^3)的演算法

對於兩個元素,如果他們型別不同,或者key不同,那麼元素就不是同一個元素,那麼直接新的元素替換前一個元素。

對於兩個元素是同一個元素的情況下,開始diff他們的附加元素,還有他們的children。

snabbdom在diff他們的children時候,一次性對比四個節點,oldNode與newNode的Children的首尾元素:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  // 開頭處理了邊界情況和特殊情況
      if (oldStartVnode == null) {
        // 如果oldStartVnode為空,那麼往後移動繼續探測
        oldStartVnode = oldCh[++oldStartIdx]; 
      } else if (oldEndVnode == null) {
        // 如果oldEndVnode為空,那麼往前移動繼續探測
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];
        // 遇到空的節點的情況總是收縮邊界搜尋,直到邊界條件跳出迴圈
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
        // 現在的首節點相同,diff他們兩個的其他屬性,並且start接著往後走
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
        // 現在的尾節點相同,diff他們兩個的其他屬性,並且old接著往前走
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm as Node, 			 api.nextSibling(oldEndVnode.elm as Node));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
        // 首尾相同的情況,對舊的節點調整孩子順序,並繼續分別收縮範圍
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 使用這裡實現了Key和Index的對應索引
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        if (isUndef(idxInOld)) { // 這是一個新的元素
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx];
        } else {
          // 元素被移動,調換元素位置
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
          }
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
//元素不是被調換的情況下,那麼建立或者刪除元素
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
複製程式碼

通過對於index與key的對應,以及特殊情況的對應,使diff演算法的平均情況能夠達到O(nlogn)。

而且根據init的注入,diff的內容還可以選擇性的加入不同內容,來優化效能。

相關文章