目前廣爲人知的React和Vue都採用了virtual-dom,Virtual DOM憑藉其高效的diff算法,讓咱們再也不關心性能問題,能夠爲所欲爲的修改數據狀態。在實際開發中,咱們並不須要關心Virtual DOM是如何實現的,可是理解Virtual DOM的實現原理確實有必要的。本文是參照https://github.com/livoras/simple-virtual-dom 源碼進行理解vitual DOM。html
若是你以爲我寫的不錯,幫我點個star前端
在日益複雜的前端應用中,狀態管理是一個常常被說起的話題,從早期的刀耕火種時代到jQuery,再到如今流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,咱們不再用維護茫茫多的事件回調、監聽來更新視圖,轉而使用使用雙向數據綁定,只須要維護相應的數據狀態,就能夠自動更新視圖,極大提升開發效率。node
可是,雙向數據綁定也並非惟一的辦法,還有一個很是粗暴有效的方式:一旦數據發生變化,從新繪製整個視圖,也就是從新設置一下innerHTML。這樣的作法確實簡單、粗暴、有效,可是若是隻是由於局部一個小的數據發生變化而更新整個視圖,性價比未免過低了,並且,像事件,獲取焦點的輸入框等,都須要從新處理。因此,對於小的應用或者說局部的小視圖,這樣處理徹底是能夠的,可是面對複雜的大型應用,這樣的作法不可取。因此咱們能夠採起用JavaScript的方法來模擬DOM樹,用新渲染的對象樹去和舊的樹進行對比,記錄下變化的變化,而後應用到真實的DOM樹上,這樣咱們只須要更改與原來視圖不一樣的地方,而不須要所有從新渲染一次。這就是virtual-DOM的優點react
相對於DOM對象,原生的JavaScript對象處理得更快,並且簡單。DOM樹上的結構,屬性信息咱們都能經過JavaScript進行表示出來,例如:git
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渲染的結果就是:github
<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樹。算法
然而光是構建DOM樹,沒什麼卵用,咱們須要將JavaScript構建的DOM樹渲染到真實的DOM樹上,用JavaScript表現一個dom一個節點很是簡單,咱們只須要記錄他的節點類型,屬性鍵值對,子節點:bash
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/justany/archive/2015/04/08/4401118.html https://github.com/livoras/blog/issues/13 https://github.com/y8n/blog/issues/5 https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060 http://www.infoq.com/cn/articles/react-dom-diff