vue在官方文檔中提到與react的渲染性能對比中,由於其使用了snabbdom而有更優異的性能。javascript
JavaScript 開銷直接與求算必要 DOM 操做的機制相關。儘管 Vue 和 React 都使用了 Virtual Dom 實現這一點,但 Vue 的 Virtual Dom 實現(復刻自 snabbdom)是更加輕量化的,所以也就比 React 的實現更高效。html
看到火到不行的國產前端框架vue也在用別人的 Virtual Dom開源方案,是否是很好奇snabbdom有何強大之處呢?不過正式解密snabbdom以前,先簡單介紹下Virtual Dom。前端
Virtual Dom能夠看作一棵模擬了DOM樹的JavaScript樹,其主要是經過vnode,實現一個無狀態的組件,當組件狀態發生更新時,而後觸發Virtual Dom數據的變化,而後經過Virtual Dom和真實DOM的比對,再對真實DOM更新。能夠簡單認爲Virtual Dom是真實DOM的緩存。vue
咱們知道,當咱們但願實現一個具備複雜狀態的界面時,若是咱們在每一個可能發生變化的組件上都綁定事件,綁定字段數據,那麼很快因爲狀態太多,咱們須要維護的事件和字段將會愈來愈多,代碼也會愈來愈複雜,因而,咱們想咱們可不能夠將視圖和狀態分開來,只要視圖發生變化,對應狀態也發生變化,而後狀態變化,咱們再重繪整個視圖就行了。java
這樣的想法雖好,可是代價過高了,因而咱們又想,能不能只更新狀態發生變化的視圖?因而Virtual Dom應運而生,狀態變化先反饋到Virtual Dom上,Virtual Dom在找到最小更新視圖,最後批量更新到真實DOM上,從而達到性能的提高。node
除此以外,從移植性上看,Virtual Dom還對真實dom作了一次抽象,這意味着Virtual Dom對應的能夠不是瀏覽器的DOM,而是不一樣設備的組件,極大的方便了多平臺的使用。若是是要實現先後端同構直出方案,使用Virtual Dom的框架實現起來是比較簡單的,由於在服務端的Virtual Dom跟瀏覽器DOM接口並無綁定關係。react
基於 Virtual DOM 的數據更新與UI同步機制:
git
初始渲染時,首先將數據渲染爲 Virtual DOM,而後由 Virtual DOM 生成 DOM。github
數據更新時,渲染獲得新的 Virtual DOM,與上一次獲得的 Virtual DOM 進行 diff,獲得全部須要在 DOM 上進行的變動,而後在 patch 過程當中應用到 DOM 上實現UI的同步更新。web
Virtual DOM 做爲數據結構,須要能準確地轉換爲真實 DOM,而且方便進行對比。
介紹完Virtual DOM,咱們應該對snabbdom的功用有個認識了,下面具體解剖下snabbdom這隻「小麻雀」。
DOM 一般被視爲一棵樹,元素則是這棵樹上的節點(node),而 Virtual DOM 的基礎,就是 Virtual Node 了。
Snabbdom 的 Virtual Node 則是純數據對象,經過 vnode 模塊來建立,對象屬性包括:
能夠看到 Virtual Node 用於建立真實節點的數據包括:
源碼:
//VNode函數,用於將輸入轉化成VNode /** * * @param sel 選擇器 * @param data 綁定的數據 * @param children 子節點數組 * @param text 當前text節點內容 * @param elm 對真實dom element的引用 * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}} */ function vnode(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return { sel: sel, data: data, children: children, text: text, elm: elm, key: key }; }
snabbdom並無直接暴露vnode對象給咱們用,而是使用h包裝器,h的主要功能是處理參數:
h(sel,[data],[children],[text]) => vnode
從snabbdom的typescript的源碼能夠看出,其實就是這幾種函數重載:
export function h(sel: string): VNode; export function h(sel: string, data: VNodeData): VNode; export function h(sel: string, text: string): VNode; export function h(sel: string, children: Array<VNode | undefined | null>): VNode; export function h(sel: string, data: VNodeData, text: string): VNode; export function h(sel: string, data: VNodeData, children: Array<VNode | undefined | null>): VNode;
建立vnode後,接下來就是調用patch方法將Virtual Dom渲染成真實DOM了。patch是snabbdom的init函數返回的。
snabbdom.init傳入modules數組,module用來擴展snabbdom建立複雜dom的能力。
很少說了直接上patch的源碼:
return function patch(oldVnode, vnode) { var i, elm, parent; //記錄被插入的vnode隊列,用於批觸發insert var insertedVnodeQueue = []; //調用全局pre鉤子 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //若是oldvnode是dom節點,轉化爲oldvnode if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } //若是oldvnode與vnode類似,進行更新 if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { //不然,將vnode插入,並將oldvnode從其父節點上直接刪除 elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } //插入完後,調用被插入的vnode的insert鉤子 for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } //而後調用全局下的post鉤子 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); //返回vnode用做下次patch的oldvnode return vnode; };
先判斷新舊虛擬dom是不是相同層級vnode,是才執行patchVnode,不然建立新dom刪除舊dom,判斷是否相同vnode比較簡單:
function sameVnode(vnode1, vnode2) { //判斷key值和選擇器 return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
patch方法裏面實現了snabbdom 做爲一個高效virtual dom庫的法寶—高效的diff算法,能夠用一張圖示意:
diff算法的核心是比較只會在同層級進行, 不會跨層級比較。而不是逐層逐層搜索遍歷的方式,時間複雜度將會達到 O(n^3)的級別,代價很是高,而只比較同層級的方式時間複雜度能夠下降到O(n)。
patchVnode函數的主要做用是以打補丁的方式去更新dom樹。
function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; //在patch以前,先調用vnode.data的prepatch鉤子 if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; //若是oldvnode和vnode的引用相同,說明沒發生任何變化直接返回,避免性能浪費 if (oldVnode === vnode) return; //若是oldvnode和vnode不一樣,說明vnode有更新 //若是vnode和oldvnode不類似則直接用vnode引用的DOM節點去替代oldvnode引用的舊節點 if (!sameVnode(oldVnode, vnode)) { var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; } //若是vnode和oldvnode類似,那麼咱們要對oldvnode自己進行更新 if (isDef(vnode.data)) { //首先調用全局的update鉤子,對vnode.elm自己屬性進行更新 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); //而後調用vnode.data裏面的update鉤子,再次對vnode.elm更新 i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } //若是vnode不是text節點 if (isUndef(vnode.text)) { //若是vnode和oldVnode都有子節點 if (isDef(oldCh) && isDef(ch)) { //當Vnode和oldvnode的子節點不一樣時,調用updatechilren函數,diff子節點 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } //若是vnode有子節點,oldvnode沒子節點 else if (isDef(ch)) { //oldvnode是text節點,則將elm的text清除 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); //並添加vnode的children addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } //若是oldvnode有children,而vnode沒children,則移除elm的children else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } //若是vnode和oldvnode都沒chidlren,且vnode沒text,則刪除oldvnode的text else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } //若是oldvnode的text和vnode的text不一樣,則更新爲vnode的text else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); } //patch完,觸發postpatch鉤子 if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }
patchVnode將新舊虛擬DOM分爲幾種狀況,執行替換textContent仍是updateChildren。
updateChildren是實現diff算法的主要地方:
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, 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; var idxInOld; var elmToMove; var before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } else if (oldEndVnode == null) { 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]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
updateChildren的代碼比較有難度,藉助幾張圖比較好理解些:
過程能夠歸納爲:oldCh和newCh各有兩個頭尾的變量StartIdx和EndIdx,它們的2個變量相互比較,一共有4種比較方式。若是4種比較都沒匹配,若是設置了key,就會用key進行比較,在比較的過程當中,變量會往中間靠,一旦StartIdx>EndIdx代表oldCh和newCh至少有一個已經遍歷完了,就會結束比較。
具體的diff分析:
對於與sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)爲true的狀況,不須要對dom進行移動。
有3種須要dom操做的狀況:
當oldStartVnode,newEndVnode相同層級時,說明oldStartVnode.el跑到oldEndVnode.el的後邊了。
當oldEndVnode,newStartVnode相同層級時,說明oldEndVnode.el跑到了newStartVnode.el的前邊。
newCh中的節點oldCh裏沒有,將新節點插入到oldStartVnode.el的前邊。
在結束時,分爲兩種狀況:
oldStartIdx > oldEndIdx,能夠認爲oldCh先遍歷完。固然也有可能newCh此時也正好完成了遍歷,統一都歸爲此類。此時newStartIdx和newEndIdx之間的vnode是新增的,調用addVnodes,把他們所有插進before的後邊,before不少時候是爲null的。addVnodes調用的是insertBefore操做dom節點,咱們看看insertBefore的文檔:parentElement.insertBefore(newElement, referenceElement)若是referenceElement爲null則newElement將被插入到子節點的末尾。若是newElement已經在DOM樹中,newElement首先會從DOM樹中移除。因此before爲null,newElement將被插入到子節點的末尾。
newStartIdx > newEndIdx,能夠認爲newCh先遍歷完。此時oldStartIdx和oldEndIdx之間的vnode在新的子節點裏已經不存在了,調用removeVnodes將它們從dom裏刪除。
shabbdom主要流程的代碼在上面就介紹完畢了,在上面的代碼中可能看不出來若是要建立比較複雜的dom,好比有attribute、props
、eventlistener的dom怎麼辦?奧祕就在與shabbdom在各個主要的環節提供了鉤子。鉤子方法中能夠執行擴展模塊,attribute、props
、eventlistener等能夠經過擴展模塊實現。
在源碼中能夠看到hook是在snabbdom初始化的時候註冊的。
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; var h_1 = require("./h"); exports.h = h_1.h; var thunk_1 = require("./thunk"); exports.thunk = thunk_1.thunk; function init(modules, domApi) { var i, j, cbs = {}; var api = domApi !== undefined ? domApi : htmldomapi_1.default; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { var hook = modules[j][hooks[i]]; if (hook !== undefined) { cbs[hooks[i]].push(hook); } } }
snabbdom在全局下有6種類型的鉤子,觸發這些鉤子時,會調用對應的函數對節點的狀態進行更改首先咱們來看看有哪些鉤子以及它們觸發的時間:
Name | Triggered when | Arguments to callback |
---|---|---|
pre |
the patch process begins | none |
init |
a vnode has been added | vnode |
create |
a DOM element has been created based on a vnode | emptyVnode, vnode |
insert |
an element has been inserted into the DOM | vnode |
prepatch |
an element is about to be patched | oldVnode, vnode |
update |
an element is being updated | oldVnode, vnode |
postpatch |
an element has been patched | oldVnode, vnode |
destroy |
an element is directly or indirectly being removed | vnode |
remove |
an element is directly being removed from the DOM | vnode, removeCallback |
post |
the patch process is done | none |
好比在patch的代碼中能夠看到調用了pre鉤子
return function patch(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); }
咱們找一個比較簡單的class模塊來看下其源碼:
function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; if (!oldClass && !klass) return; if (oldClass === klass) return; oldClass = oldClass || {}; klass = klass || {}; for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } exports.classModule = { create: updateClass, update: updateClass }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.classModule; },{}]},{},[1])(1) });
能夠看出create和update鉤子方法調用的時候,能夠執行class模塊的updateClass:從elm中刪除vnode中不存在的或者值爲false的類
將vnode中新的class添加到elm上去
。
https://github.com/snabbdom/snabbdom/
https://segmentfault.com/a/1190000009017324
https://segmentfault.com/a/1190000009017349
http://web.jobbole.com/90831/