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

snabbdom 是什麼

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

從Vue2發佈開始,也開始使用了這樣的機制。Vue並無選擇本身從新造一套Virtual-DOM的算法,而是在snabbdom的基礎上構建了一個嵌入了框架自己的fork版本。能夠說,Vue就是在使用snabbdom的Virtual-DOM算法。java

snabbdom 的特性

  • snabbdom核心算法就兩三百多行,閱讀和理解都是很是方便的。
  • module劃分清楚,拓展性強
  • 自帶一系列hook,這些hook能夠在diff算法的各處調用,可使用hook定製過程
  • 在Virtual-DOM衆多算法中有着優秀的性能
  • 函數都帶有和本身簽名相關的reduce/scan函數,方便函數響應式編程使用
  • h函數能夠簡單的建立vnode節點
  • 對於SVG,使用h函數能夠輕鬆加上命名空間

snabbdom核心概念

  • initnode

    snabbdom使用一種相似於插件聲明使用的方式來模塊化功能,若是你使用過AngularJS的聲明注入或者Vue.use,你對這樣的方式必定不陌生。react

    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函數api

    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)dom

    名稱 觸發時間 回調參數
    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的內容還能夠選擇性的加入不一樣內容,來優化性能。

相關文章
相關標籤/搜索