目前廣爲人知的React和Vue都採用了virtual-dom,Virtual DOM憑藉其高效的diff算法,讓咱們再也不關心性能問題,能夠爲所欲爲的修改數據狀態。在實際開發中,咱們並不須要關心Virtual DOM是如何實現的,可是理解Virtual DOM的實現原理確實有必要的。本文是參照https://github.com/livoras/si... DOM。html
在日益複雜的前端應用中,狀態管理是一個常常被說起的話題,從早期的刀耕火種時代到jQuery,再到如今流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,咱們不再用維護茫茫多的事件回調、監聽來更新視圖,轉而使用使用雙向數據綁定,只須要維護相應的數據狀態,就能夠自動更新視圖,極大提升開發效率。前端
可是,雙向數據綁定也並非惟一的辦法,還有一個很是粗暴有效的方式:一旦數據發生變化,從新繪製整個視圖,也就是從新設置一下innerHTML。這樣的作法確實簡單、粗暴、有效,可是若是隻是由於局部一個小的數據發生變化而更新整個視圖,性價比未免過低了,並且,像事件,獲取焦點的輸入框等,都須要從新處理。因此,對於小的應用或者說局部的小視圖,這樣處理徹底是能夠的,可是面對複雜的大型應用,這樣的作法不可取。因此咱們能夠採起用JavaScript的方法來模擬DOM樹,用新渲染的對象樹去和舊的樹進行對比,記錄下變化的變化,而後應用到真實的DOM樹上,這樣咱們只須要更改與原來視圖不一樣的地方,而不須要所有從新渲染一次。這就是virtual-DOM的優點node
相對於DOM對象,原生的JavaScript對象處理得更快,並且簡單。DOM樹上的結構,屬性信息咱們都能經過JavaScript進行表示出來,例如:react
var element = { tagName: 'ul', // 節點標籤名 props: { // dom的屬性鍵值對 id: 'list' }, children: [ {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]} ] }
那麼在html渲染的結果就是:git
<ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul>
既然可以經過JavaScript表示DOM樹的信息,那麼就能夠經過使用JavaScript來構建DOM樹。github
然而光是構建DOM樹,沒什麼卵用,咱們須要將JavaScript構建的DOM樹渲染到真實的DOM樹上,用JavaScript表現一個dom一個節點很是簡單,咱們只須要記錄他的節點類型,屬性鍵值對,子節點:算法
function Element(tagName, props, children) { this.tagName = tagName this.props = props this.children = children }
那麼ul標籤咱們就可使用這種方式來表示app
var ul = new Element('ul', {id: 'list'}, [ {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]} ])
說了這麼多,他只是用JavaScript表示的一個結構,那該如何將他渲染到真實的DOM結構中呢:dom
Element.prototype.render = function() { let el = document.createElement(this.tagName), // 節點名稱 props = this.props // 節點屬性 for (var propName in props) { propValue = props[propName] el.setAttribute(propName, propValue) } this.children.forEach((child) => { var childEl = (child instanceof Element) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el }
若是咱們想將ul渲染到DOM結構中,就只須要性能
ulRoot = ul.render() document.appendChild(ulRoot)
這樣就完成了ul到DOM的渲染,也就有了真正的DOM結構
<ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul>
React的核心算法是diff算法(這裏指的是優化後的算法)咱們來看看diff算法是如何實現的:
diff只會對相同顏色方框內的DOM節點進行比較,即同一個父節點下的全部子節點。當發現節點不存在,則該節點和子節點會被徹底刪除,不會作進一步的比較。
在實際的代碼中,會對新舊兩棵樹進行深度的遍歷,給每個節點進行標記。而後在新舊兩棵樹的對比中,將不一樣的地方記錄下來。
// diff 算法,對比兩棵樹 function diff(oldTree, newTree) { var index = 0 // 當前節點的標誌 var patches = {} // 記錄每一個節點差別的地方 dfsWalk(oldTree, newTree, index, patches) return patches } function dfsWalk(oldNode, newNode, index, patches) { // 對比newNode和oldNode的差別地方進行記錄 patches[index] = [...] diffChildren(oldNode.children, newNode.children, index, patches) } function diffChildren(oldChildren, newChildren, index, patches) { let leftNode = null var currentNodeIndex = index oldChildren.forEach((child, i) => { var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標記 ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) // 遍歷子節點 leftNode = child }) }
例如:
在圖中若是div有差別,標記爲0,那麼:
patches[0] = [{difference}, {difference}]
同理,有p是patches[1], ul是patches[3],以此類推
patches指的是差別變化,這些差別包括:一、節點類型的不一樣,二、節點類型相同,可是屬性值不一樣,文本內容不一樣。因此有這麼幾種類型:
var REPLACE = 0, // replace 替換 REORDER = 1, // reorder 父節點中子節點的操做 PROPS = 2, // props 屬性的變化 TEXT = 3 // text 文本內容的變化
若是節點類型不一樣,就說明是須要替換,例如將div替換成section,就記錄下差別:
patches[0] = [{ type: REPLACE, node: newNode // section },{ type: PROPS, props: { id: 'container' } }]
在標題二中構建了真正的DOM樹的信息,因此先對那一棵DOM樹進行深度優先的遍歷,遍歷的時候同
patches對象進行對比,找到其中的差別,而後應用到DOM操做中。
function patch(node, patches) { var walker = {index: 0} // 記錄當前節點的標誌 dfsWalk(node, walker, patches) } function dfsWalk(node, walker, patches) { var currentPatches = patches[walker.index] // 這是當前節點的差別 var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍歷子節點 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 對當前節點進行DOM操做 } } // 將差別的部分應用到DOM中 function applyPatches(node, currentPatches) { currentPatches.forEach((currentPatch) => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break; case REORDER: reorderChldren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: if (node.textContent) { node.textContent = currentPatch.content } else { node.nodeValue = currentPatch.content } break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
此次的粗糙的virtual-dom基本已經實現了,具體的狀況更加複雜。但這已經足夠讓咱們理解virtual-dom。
具體的帶解析的代碼已經上傳到github
https://www.cnblogs.com/justa...
https://github.com/livoras/bl...
https://github.com/y8n/blog/i...
https://medium.com/@deathmood...
http://www.infoq.com/cn/artic...