前言
目前廣為人知的React和Vue都採用了virtual-dom,Virtual DOM憑藉其高效的diff演算法,讓我們不再關心效能問題,可以隨心所欲的修改資料狀態。在實際開發中,我們並不需要關心Virtual DOM是如何實現的,但是理解Virtual DOM的實現原理確實有必要的。本文是參照github.com/livoras/sim… 原始碼進行理解vitual DOM。
一、前端應用狀態管理
在日益複雜的前端應用中,狀態管理是一個經常被提及的話題,從早期的刀耕火種時代到jQuery,再到現在流行的MVVM時代,狀態管理的形式發生了翻天覆地的變化,我們再也不用維護茫茫多的事件回撥、監聽來更新檢視,轉而使用使用雙向資料繫結,只需要維護相應的資料狀態,就可以自動更新檢視,極大提高開發效率。
但是,雙向資料繫結也並不是唯一的辦法,還有一個非常粗暴有效的方式:一旦資料發生變化,重新繪製整個檢視,也就是重新設定一下innerHTML。這樣的做法確實簡單、粗暴、有效,但是如果只是因為區域性一個小的資料發生變化而更新整個檢視,價效比未免太低了,而且,像事件,獲取焦點的輸入框等,都需要重新處理。所以,對於小的應用或者說區域性的小檢視,這樣處理完全是可以的,但是面對複雜的大型應用,這樣的做法不可取。所以我們可以採取用JavaScript的方法來模擬DOM樹,用新渲染的物件樹去和舊的樹進行對比,記錄下變化的變化,然後應用到真實的DOM樹上,這樣我們只需要更改與原來檢視不同的地方,而不需要全部重新渲染一次。這就是virtual-DOM的優勢
二、檢視渲染
相對於DOM物件,原生的JavaScript物件處理得更快,而且簡單。DOM樹上的結構,屬性資訊我們都能通過JavaScript進行表示出來,例如:
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渲染的結果就是:
<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一個節點非常簡單,我們只需要記錄他的節點型別,屬性鍵值對,子節點:
function Element(tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}複製程式碼
那麼ul標籤我們就可以使用這種方式來表示
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結構中呢:
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>複製程式碼
三、比較虛擬DOM樹的差異
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指的是差異變化,這些差異包括:1、節點型別的不同,2、節點型別相同,但是屬性值不同,文字內容不同。所以有這麼幾種型別:
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樹的資訊,所以先對那一棵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
六、 References
www.cnblogs.com/justany/arc…
github.com/livoras/blo…
github.com/y8n/blog/is…
medium.com/@deathmood/…
www.infoq.com/cn/articles…
來源作者:skychenbo
來源連結:https://juejin.im/post/5a1fccadf265da432153d32b
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。