VirtualDOM是react在組件化開發場景下,針對DOM重排重繪性能瓶頸做出的重要優化方案,而他最具價值的核心功能是如何識別並保存新舊節點數據結構之間差別的方法,也便是diff算法。毫無疑問的是diff算法的複雜度與效率是決定VirtualDOM可以帶來性能提高效果的關鍵因素。所以,在VirtualDOM方案被提出以後,社區中不斷涌現出對diff的改進算法,引用司徒正美的經典介紹:html
最開始經典的深度優先遍歷DFS算法,其複雜度爲O(n^3),存在高昂的diff成本,而後是cito.js的橫空出世,它對從此全部虛擬DOM的算法都有重大影響。它採用兩端同時進行比較的算法,將diff速度拉高到幾個層次。緊隨其後的是kivi.js,在cito.js的基出提出兩項優化方案,使用key實現移動追蹤及基於key的編輯長度距離算法應用(算法複雜度 爲O(n^2))。但這樣的diff算法太過複雜了,因而後來者snabbdom將kivi.js進行簡化,去掉編輯長度距離算法,調整兩端比較算法。速度略有損失,但可讀性大大提升。再以後,就是著名的vue2.0 把snabbdom整個庫整合掉了。vue
所以目前VirtualDOM的主流diff算法趨向一致,在主要diff思路上,snabbdom與react的reconilation方式基本相同。
若是沒有理解virtual dom的構建思想,那麼你能夠參考這篇精緻文章Boiling React Down to a Few Lines in jQuery
virtual dom優化開發的方式是:經過vnode,來實現無狀態組件,結合單向數據流(undirectional data flow),進行UI更新,總體代碼結構是:node
var newVnode = render(vnode, state) var oldVnode = patch(oldVnode, newVnode) state.dispatch('change') var newVnode = render(vnode, state) var oldVnode = patch(oldVnode, newVnode)
在衆多virtual dom庫中,咱們選擇snabbdom庫,緣由有不少:react
1.snabbdom性能排名靠前,雖然這個benchmark的參考性不高
2。snabbdom示例豐富
3.snabbdom具備必定的生態圈,如motorcycle.js,cycle-snabbdom,cerebral
4.snabbdom實現的十分優雅,使用的是recursive方式調用patch,對比infernojs優化痕跡明顯的代碼,snabbdom更易讀。
5.在閱讀過程當中發現,snabbdom的模塊化,插件支持作得極佳webpack
咱們來查看snabbdom基本使用方式。web
// snabbdom在./snabbdom.js var snabbdom = require('snabbdom') // 初始化snabbdom,獲得patch。隨後,咱們能夠看到snabbdom設計的精妙之處 var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]) // h是一個生成vnode的包裝函數,factory模式?對生成vnode更精細的包裝就是使用jsx // 在工程裏,咱們一般使用webpack或者browserify對jsx編譯 var h = require('snabbdom/h') // 構造一個virtual dom,在實際中,咱們一般但願一個無狀態的vnode // 而且咱們經過state來創造vnode // react使用具備render方法的對象來做爲組件,這個組件能夠接受props和state // 在snabbdom裏面,咱們一樣能夠實現相似效果 // function component(state){return h(...)} var vnode = h( 'div#container.two.classes', {on: {click: someFn}}, [ h('span', {style: {fontWeight: 'bold'}}, 'This is bold'), ' and this is just normal text', h('a', {props: {href: '/foo'}}, 'I\'ll take you places!') ] ) // 獲得初始的容器,注意container是一個dom element var container = document.getElementById('container') // 將vnode patch到container中 // patch函數會對第一個參數作處理,若是第一個參數不是vnode,那麼就把它包裝成vnode // patch事後,vnode發生變化,表明瞭如今virtual dom的狀態 patch(container, vnode) // 建立一個新的vnode var newVnode = h( 'div#container.two.classes', {on: {click: anotherEventHandler}}, [ h('span', {style: {fontWeight: 'normal', fontStyle: 'italics'}}, 'This is now italics'), ' and this is still just normal text', h('a', {props: {href: '/bar'}}, 'I\'ll take you places!') ] ) // 將新的vnode patch到vnode上,如今newVnode表明vdom的狀態 patch(vnode, newVnode)
vnode的定義在./vnode.js中 vnode具有的屬性
1.tagName 能夠是custom tag,能夠是'div','span',etc,表明這個virtual dom的tag name
2.data, virtual dom數據,它們與dom element的prop、attr的語義相似。可是virtual dom包含的數據能夠更靈活。
好比利用./modules/class.js插件,咱們在data裏面輕鬆toggle一個類名算法
h('p', {class: {'hide': hideIntro}})
隨後是h函數的包裝api
h的實如今./h.js數組
包裝函數一共注意三點瀏覽器
與dom api的對接
實如今./htmldomapi.js
採用adapter模式,對dom api進行包裝,而後將htmldomapi做爲默認的瀏覽器接口
這種設計很機智。在擴展snabbdom的兼容性的時候,只須要改變snabbdom.init使用的瀏覽器接口,而不用改變patch等方法的實現
snabbdom的核心內容實如今./snabbdom.js。snabbdom的核心實現不到三百行(233 sloc),很是簡短。
在snabbdom裏面實現了snabbdom的virtual dom diff算法與virtual dom lifecycle hook支持。
virtual dom diff
vdom diff是virtual dom的核心算法,snabbdom的實現原理與react官方文檔Reconciliation一致
總結起來有:
一篇閱讀量頗豐的文章React’s diff algorithm也說明的就是啓發過程,惋惜,沒有實際的代碼參照。如今,咱們根據snabbdom代碼來看啓發規則的運用,結束後,你會明白virtual dom的實現有多簡單。
首先來到snabbdom.js中init函數的return語句
return function(oldVnode, vnode) { var i, elm, parent; // insertedVnodeQueue存在於整個patch過程 // 用於收集patch中新插入的vnode var insertedVnodeQueue = []; // 在進行patch以前,咱們須要運行prepatch hook // cbs是init函數變量,即,這個return語句中函數的閉包 // 這裏,咱們不理會lifecycle hook,而只關注vdom diff算法 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 若是oldVnode不是vnode(在第一次調用時,oldVnode是dom element) // 那麼用emptyNodeAt函數來將其包裝爲vnode if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } // sameVnode是上述「值不值得patch」的核心 // sameVnode實現很簡單,查看兩個vnode的key與sel是否分別相同 // ()=>{vnode1.key === vnode2.key && vnode1.sel === vnode2. // 比較語義不一樣的結構沒有意義,好比diff一個'div'和'span' // 而應該移除div,根據span vnode插入新的span // diff兩個key不相同的vnode一樣沒有意義 // 指定key就是爲了區分element // 對於不一樣key的element,不該該去根據newVnode來改變oldVnode的數據 // 而應該移除再也不oldVnode,添加newVnode if (sameVnode(oldVnode, vnode)) { // oldVnode與vnode的sel和key分別相同,那麼這兩個vnode值得去比較 //patchVnode根據vnode來更新oldVnode patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { //不值得去patch的,咱們就暴力點 // 移除oldVnode,根據newVnode建立elm,並添加至parent中 elm = oldVnode.elm; parent = api.parentNode(elm); // createElm根據vnode建立element createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 將新建立的element添加到parent中 api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); // 同時移除oldVnode removeVnodes(parent, [oldVnode], 0, 0); } } // 結束之後,調用插入vnode的insert hook for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } // 整個patch結束,調用cbs中的post hook for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; }; ``` ###而後咱們閱讀patch的過程 ``` function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; // 如前,在patch以前,調用prepatch hook,可是這個是vnode在data裏定義的prepatch hook,而不是全局定義的prepatch hook 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引用相同,則不必比較。在良好設計的vdom裏,大部分時間咱們都在執行這個返回語句。 if (oldVnode === vnode) return; // 若是兩次引用不一樣,那說明新的vnode建立了 // 與以前同樣,咱們先看這兩個vnode值不值得去patch if (!sameVnode(oldVnode, vnode)) { // 這四條語句是否與init返回函數裏那四條相同? var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; } // 這兩個vnode值得去patch // 咱們先patch vnode,patch的方法就是先調用全局的update hook // 而後調用vnode.data定義的update hook if (isDef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } // patch兩個vnode的text和children // 查看vnode.text定義 // vdom中規定,具備text屬性的vnode不該該具有children // 對於<p>foo:<b>123</b></p>的良好寫法是 // h('p', [ 'foo:', h('b', '123')]), 而非 // h('p', 'foo:', [h('b', '123')]) if (isUndef(vnode.text)) { // vnode不是text node,咱們再查看他們是否有children if (isDef(oldCh) && isDef(ch)) { // 兩個vnode都有children,那麼就調用updateChildren if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { // 只有新的vnode有children,那麼添加vnode的children if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { // 只有舊vnode有children,那麼移除oldCh removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { // 二者都沒有children,而且oldVnode.text不爲空,vnode.text未定義,則清空elm.textContent api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // vnode是一個text node,咱們改變對應的elm.textContent // 在這裏咱們使用api.setText api api.setTextContent(elm, vnode.text); } if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }
patch的實現是否簡單明瞭?甚至有以爲「啊?這就patch完了」的感受。固然,咱們還差最後一個,這個是重頭戲——updateChildren。
updateChildren的代碼較長且密集,可是算法十分簡單
oldCh是一個包含oldVnode的children數組,newCh同理
咱們先遍歷兩個數組(while語句),維護四個變量
當oldStartIdx > oldEndIdx或者newStartIdx > newOldStartIdx的時候中止遍歷。
遍歷過程當中有五種比較
前四種比較
最後一種比較
利用vnode.key,在ul>li*n的結構裏,咱們頗有可能使用key來標誌li的惟一性,那麼咱們就會來到最後一種狀況。這個時候,咱們先產生一個index-key表(createKeyToOldIdx),而後根據這個表來進行更改。
更改規則
若是newVnode.key不在表中,那麼這個newVnode就是新的vnode,將其插入
若是newVnode.key在表中,那麼對應的oldVnode存在,咱們須要patch這兩個vnode,並在patch以後,將這個oldVnode置爲undefined(oldCh[idxInOld] = undefined),同時將oldVnode.elm位置變換到當前oldStartIdx以前,以避免影響接下來的遍歷
遍歷結束後,檢查四個變量,對移除剩餘的oldCh或添加剩餘的newCh
patch總結
閱讀完init函數return語句,patch,updateChildren,咱們能夠理解整個diff和patch的過程
有些函數createElm,removeVnodes並不重要
閱讀完virtual dom diff算法實現後,咱們可能會奇怪,關於style、class、attr的patch在哪裏?這些實現都在modules,並經過lifecycle發揮做用
snabbdom的生命週期鉤子函數定義在core doc - hook中。
再查看modules裏的class會發現,class module經過兩個hook鉤子來對elm的class進行patch。這兩個鉤子是create和update。
回到init函數,這兩個鉤子在函數體開頭註冊
for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } }
create hook在createElm中調用。createElm是惟一添加vnode的方法,因此insertedVnodeQueue.push只發生在createElm中。