snabbdom是一個Virtual-DOM的實現庫,它專一於使用的簡單以及功能和的模型化,並在效率和性能上有着很好的表現。若是你還不知道什麼是Virtual-DOM技術,它是一種網頁中經過diff算法來實現網頁修改最小化的方法,react底層使用了這樣的機制來提升性能。javascript
從Vue2發佈開始,也開始使用了這樣的機制。Vue並無選擇本身從新造一套Virtual-DOM的算法,而是在snabbdom的基礎上構建了一個嵌入了框架自己的fork版本。能夠說,Vue就是在使用snabbdom的Virtual-DOM算法。java
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 |
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的內容還能夠選擇性的加入不一樣內容,來優化性能。