咱們知道不論是vue仍是react當中,都是利用virtual dom(下面簡稱vd)來表示真實的dom,由於操做真實的dom的代價是昂貴的,即便是查找dom節點的操做都是昂貴的,因此在優化的方法當中,就有緩存dom的查找結果的一個優化,那麼既然真實dom的操做是昂貴的,因此若是咱們在使用diff算法來比較兩個dom之間的差別的時候,就要遍歷全部的dom來進行對比,若是是按照真實的dom來進行diff算法的比較的話,那麼就至關消耗性能了,所以vd應運而生。那麼怎麼將真實的dom和vd對應起來呢?咱們知道,dom不外乎三個特性:
一、標籤名
二、各類屬性
三、孩子節點
所以,若是要用vd來表示dom的話咱們就能夠這樣定義。vue
class VNode {
constructor(tagName, attributes, children) {
this.tagName = tagName
this.attributes = attributes
this.children = children
}
}
複製代碼
好比有這樣的domnode
<div id="div" class="classVal">
<span>child</span>
</div>
複製代碼
那麼vd就是這樣的react
{
tagName: 'div',
attributes: {
'id': 'div',
'class': 'classVal'
},
children: [{
tagName: 'span',
attributes: null,
children: ['child']
}]
}
複製代碼
固然,這裏vd的定義少了TEXT節點,因此咱們加上TEXT節點,TEXT節點直接返回裏面的innerText/textContent,就像上面的children: ['child']
,咱們定義一個叫h的函數,用來建立vd,包括TEXT節點,它接受四個參數,分別以下:git
tagName: 標籤名
text: 若是是TEXT節點,那麼就是TEXT的內容,即innerText/textContent
attributes: dom屬性的價值對
children: dom的孩子vd
複製代碼
function h(tagName, text, attributes, children) {
// 判斷到是TEXT節點,直接返回TEXT裏面的內容
if(text) {
return text
}
return new VNode(tagName, attributes, children)
}
複製代碼
好了,VD大概就是這樣子表示,那麼咱們若是根據vd還原成真實的dom呢,其實很簡單,就是根據一一對應關係還原唄:github
function createElement(vnode) {
var el = null;
// 文本元素
if(typeof vnode === "string") {
el = document.createTextNode(vnode);
return el;
}
// 還原dom
el = document.createElement(vnode.tagName);
// 還原attribute
for(var key in attributes) {
el.setAttribute(key, attributes[key]);
}
// 還原孩子節點
var children = vnode.children.map(createElement);
children.forEach(function(child) {
el.appendChild(child);
});
return el;
}
複製代碼
關於vd的理解差很少就這樣,若是有須要補充的或者指正的,望不吝賜教。算法
有了vd後,咱們要怎麼比較兩個dom樹之間的不一樣呢,固然不能無腦的使用innerHTML對整塊樹更新(backbone就是這樣),而是針對更改的地方進行更新或者替換,那麼咱們就須要依賴diff來找出兩棵樹之間的不一樣。
傳統的diff算法,是須要跨級對比兩個樹之間的不一樣,時間複雜度爲O(n^3),這樣的對比是沒法接受的,因此react提出了一個簡單粗暴的diff算法,只對比同級元素,這樣算法複雜度就變成了O(n)了,雖然不能作到最優的更新,可是時間複雜度大大減小,是一種平衡的算法,下面會提到。緩存
那麼怎麼理解它是隻對比同級和具體它是怎麼對比的呢?
基於diff算法的同級對比,咱們先講下對比的過程當中,它主要分爲四種類型的對比,分別爲:
一、新建create: 新的vd中有這個節點,舊的沒有
二、刪除remove: 新的vd中沒有這個節點,舊的有
三、替換replace: 新的vd的tagName和舊的tagName不一樣
四、更新update: 除了上面三點外的不一樣,具體是比較attributes先,而後再比較children
寫成代碼就是這樣:bash
diff(newVnode, oldVNode) {
if(!newVNode) {
// 新節點中沒有,說明是刪除舊節點的
return {
type: 'remove'
}
} else if(!oldVNode) {
// 新節點中有舊節點沒有的,說明是刪除
return {
type: 'create',
newVNode
}
} else if(isDiff(newVNode, oldVNode)) {
// 只要對比出兩個節點的tagName不一樣,說明是替換
return {
type: 'replace',
newVNode
}
} else {
// 其餘狀況是更新節點,要對比兩個節點的attributes和孩子節點
return {
type: 'update',
attributes: diffAttributes(newVNode, oldVNode),
children: diffChildren(newVNode, oldVNode)
}
}
}
// 對比孩子節點,其實就是遍歷全部的孩子節點,而後調用diff對比
function diffChildren(newVnode, oldVNode) {
var patches = []
// 這裏要獲取兩個節點中的最大孩子數,而後再進行對比
var len = Math.max(newVnode.children.length, oldVNode.children.length);
for(let i = 0; i <len; i++) {
patches[i] = diff(newVnode.children[i], oldVnode.children[i])
}
return patches
}
// 對比attribute,只有兩種狀況,要不就是值改變/新建,要不就是刪除值,對比dom只有setAttribute和removeAttribute就知道了
function diffAttributes(newVnode, oldVNode) {
var patches = []
// 獲取新舊節點的全部attributes
var attrs = Object.assign({}, oldVNode.attributes, newVNode.attributes)
for(let key in attrs) {
let value = attrs[key]
// 只要新節點的屬性值和久節點的屬性值不一樣,就判斷爲新建,不論是更新和真正的新建都是調用setAttribute來更新
if(oldVNode.attributes[key] !== value) {
patches.push({
type: 'create',
key,
value: newVnode.attributes[key]
})
} else if(!newVNode.attributes[key]) {
patches.push({
key,
type: 'remove'
})
}
}
return patches
}
// 判斷兩個節點是否不一樣
function isDiff(newVNode, oldVNode) {
// 正常狀況下,只對比tagName,可是text節點對比沒有tagName,因此要考慮text節點
return (typeof newVNode === 'string' && newVNode !== oldVNode)
|| (typeof oldVNode === 'string' && newVNode !== oldVNode)
|| newVNode.tagName !== oldVNode.tagName
}
複製代碼
結合代碼,你們對比下面的圖,圖裏面remove沒有列出來,remove和create差很少緣由,相信你們知道什麼狀況下是remove。 mvc
上圖,按傳統的方法只是在span和p直接插入了一個div,可是diff算法不是這麼來更新的,它只對比同一級別的,即會以爲舊節點的p和新節點的div纔是同一級,他們的tagName不一樣,因此定義爲replace,接着把舊節點的div當作和新節點的p是同一級,依舊是replace,最後舊節點沒有div,因此create了。能夠看到,其實這個更新代價仍是比較大的,可是比對的過程卻簡單和快速,所以是一種相對平衡的算法。完整的代碼你們能夠看下個人git地址:github.com/VikiLee/XLM…app
咱們更新dom的時候,儘可能不要整棵樹進行更新,須要作到細顆粒的更新,要作到細顆粒地更新就必須知道兩棵樹直接的不一樣,因此須要使用diff算法來進行對比,可是傳統的diff算法雖然能作到細顆粒準確地更新,可是它須要花銷大量的時間來進行比對,因此有來react的改版的diff算法,只比較同一級的元素,這樣能夠作到快速的比對,爲O(n),即便這樣,在對比兩棵樹的時候,咱們仍是須要遍歷全部的節點,咱們知道dom的操做是昂貴的,即便是查找,也是昂貴的一個過程,特別是在節點不少的donm樹下,因此虛擬dom應運而生,虛擬dom避開了直接操做dom的缺點,而是直接對比內存中vd,使得對比速度進一步獲得質地提高。