把一個div
元素的屬性打印出來,以下:javascript
DOM
的元素是很是龐大的,這也是
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>
複製代碼
DOM
樹的信息能夠用JavaScript
對象表示出來,則說明能夠用JavaScript
對象去表示樹結構來構建一棵真正的DOM
樹。java
狀態變動->從新渲染整個視圖的方式能夠用新渲染的對象樹去和舊的樹進行對比,記錄這兩棵樹的差別。二者的不一樣之處就是咱們須要對頁面真正的DOM
操做,而後把它們應用在真正的DOM
樹上,頁面就變動了。這樣能夠作到:視圖的結構確實是整個全新渲染了,可是最後操做DOM
的只有變動不一樣的地方。node
DOM
樹的結構,而後用這個樹構建一個真正的DOM
樹,插到文檔當中2
所記錄的差別應用到步驟1所構建的的真正的DOM
樹上,視圖就更新了Virtual DOM
本質就是在JS和DOM之間作了一個緩存,JS
操做Virtual DOM
,最後再應用到真正的DOM
上。git
步驟一:用JS
對象模擬虛擬DOM
樹github
用JavaScript
來表示一個DOM
節點,則須要記錄它的節點類型、屬性、子節點: element.js算法
function Element (tagName, props, children) {
this.tagName = tagName
this.props = props
this.children = children
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children)
}
複製代碼
上面的DOM結構能夠表示爲:數組
var el = require('./element')
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])
複製代碼
如今ul
只是一個JavaScript
對象表示的DOM
結構,頁面上並無這個結構。能夠根據這個ul
構建真正的<ul>
:緩存
Element.prototype.render = function () {
var el = document.createElement(this.tagName) // 根據tagName構建
var props = this.props
for (var propName in props) { // 設置節點的DOM屬性
var propValue = props[propName]
el.setAttribute(propName, propValue)
}
var children = this.children || []
children.forEach(function (child) {
var childEl = (child instanceof Element)
? child.render() // 若是子節點也是虛擬DOM,遞歸構建DOM節點
: document.createTextNode(child) // 若是字符串,只構建文本節點
el.appendChild(childEl)
})
return el
}
複製代碼
render
方法會根據tagName
構建一個真正的DOM
節點,而後設置這個節點的屬性,最後遞歸地把本身的子節點也構建起來。因此須要:app
var ulRoot = ul.render()
document.body.appendChild(ulRoot)
複製代碼
上面的ulRoot
是真正的DOM
節點,把它塞進文檔中,這樣body
裏面就有了真正的<ul>
的DOM結構:
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
複製代碼
步驟二:比較兩棵虛擬DOM樹的差別
比較兩棵DOM
樹的差別是Virtual DOM
算法最核心的部分,就是diff
算法。兩棵樹的徹底diff
算法是一個時間複雜度爲O(n^3)
的問題。但在前端中,不多會跨越層級地移動DOM
元素。因此Virtual DOM
只會對同一層級的元素進行對比:
div
只會和同一層級的
div
對比,第二層級的只會跟第二層級對比。這樣算法複雜度就能夠達到
O(n)
。
a.深度優先遍歷,記錄差別 在實際的代碼中,會對新舊兩棵樹進行一個深度優先的遍歷,這樣每一個節點都會有一個惟一的標記:
// diff 函數,對比兩棵樹
function diff (oldTree, newTree) {
var index = 0 // 當前節點的標誌
var patches = {} // 用來記錄每一個節點差別的對象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
// 對比oldNode和newNode的不一樣,記錄下來
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
// 遍歷子節點
function diffChildren (oldChildren, newChildren, index, patches) {
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach(function (child, i) {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 計算節點的標識
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍歷子節點
leftNode = child
})
}
複製代碼
例如,上面的div和新的div有差別,當前的標記是0
,那麼:
patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不一樣
複製代碼
同理p
是patches[1]
,ul
是patches[3]
,以此類推
b.差別類型
對DOM
操做會有的差別:
div
換成了section
div
的子節點,把p
和ul
順序互換2
內容爲Virtual DOM2
因此定義了幾種差別類型:
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3
複製代碼
對於節點的替換,判斷新舊節點的tagName
和是否是同樣,若是不同就替換掉。如div
換成section
,記錄以下:
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}]
複製代碼
若是給div
新增了屬性id
爲container
,記錄以下:
patches[0] = [{
type: REPALCE,
node: newNode // el('section', props, children)
}, {
type: PROPS,
props: {
id: "container"
}
}]
複製代碼
若是修改文本節點,如上面的文本節點2
,記錄以下:
patches[2] = [{
type: TEXT,
content: "Virtual DOM2"
}]
複製代碼
c.列表對比算法
上面若是把div
中的子節點從新排序,看如p
,ul
,div
的順序換成了div
,p
,ul
。按照同層進行順序對比的話,它們都會被替換掉,這樣DOM
開銷很是大。而實際上只須要經過節點移動就能夠的了。 假設如今能夠英文字母惟一得標誌每個子節點: 舊的節點順序: a b c d e f g h i
如今對節點進行刪除、插入、移動的操做。新增j節點,刪除e節點,移動h節點: 新的節點順序: a b c h d f g i j
如今知道了新舊的順序,求最小的插入、刪除操做(移動能夠當作是刪除和插入操做的結合)。這個問題抽象出來實際上是字符串的最小編輯距離問題(Edition Distance
),最多見的算法是Levenshtein Distance
, 經過動態規劃求解,時間複雜度爲O(M*N)
。而咱們只須要優化一些常見的移動操做,犧牲必定的DOM
操做,讓算法時間複雜度達到線性的O((max(M,N)))
。 獲取某個父節點的子節點的操做,就能夠記錄以下:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
複製代碼
因爲tagName
是能夠重複的,因此不能用這個來進行對比。須要給子節點加上一盒惟一標識key
,列表對比的時候,使用key
進行對比,這樣就能複用舊的DOM
樹上的節點。 經過深度優先遍歷兩棵樹,每層節點進行對比,記錄下每一個節點的差別。完整的diff
算法訪問:github.com/livoras/sim…
步驟三:把差別應用到真正的DOM
樹上 由於步驟一所構建的JavaScript
對象樹和render
出來的真正的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] // 從patches拿出當前節點的差別
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操做
}
}
複製代碼
applyPatches
,根據不一樣類型的差別對當前節點進行 DOM
操做:
function applyPatches (node, currentPatches) {
currentPatches.forEach(function (currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node)
break
case REORDER:
reorderChildren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
node.textContent = currentPatch.content
break
default:
throw new Error('Unknown patch type ' + currentPatch.type)
}
})
}
複製代碼
完整patch
代碼訪問:github.com/livoras/sim…