Vue在2.0版本引入了虛擬DOM。其了虛擬DOM算法是基於snabbdom算法所作的修改。參看github.com/vuejs/vue/b…註釋部分。要想了解Vue,必須瞭解虛擬DOM,本篇文章主要介紹了什麼是虛擬DOM,爲何用虛擬DOM以及其具體實現。html
用JavaScript模擬DOM樹造成虛擬DOM樹,以下面的html結構前端
<ul style="color:#000">
<li>蘋果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
複製代碼
可使用以下JS表示vue
{
sel: 'ul',
data: { style: {color: '#000'}}, // 節點屬性及綁定事件等
children: [ // 子節點
{sel: 'li', text: '蘋果'},
{sel: 'li', text: '香蕉'},
{sel: 'li', text: '橙子'}
]
}
複製代碼
由於對DOM的直接操做是很是慢並且低效的。瀏覽器的渲染流程包括解析html以構建dom樹->構建render樹->佈局render樹->繪製render樹,而每一次DOM改變從構建render樹到佈局到渲染都要重來。參考文檔node
而虛擬DOM的優點就是:1.開發者再也不關心DOM而只關心數據,提高開發效率。2.保證最小化的DOM操做,使執行效率獲得提高。react
虛擬DOM的優點並不在於它操做DOM比較快,而是可以經過虛擬DOM的比較,最小化真實DOM操做,參考文檔git
實現虛擬DOM包含如下三個步驟github
用JavaScript模擬DOM樹造成虛擬DOM樹算法
當組件狀態發生更新時,比較新舊虛擬DOM樹segmentfault
將差別應用到真正的DOM上 api
虛擬DOM對象包含如下屬性:
給定任意兩棵樹,找到最少的轉換步驟。可是標準的的Diff算法複雜度須要O(n^3).
這顯然沒法知足性能的要求,考慮到前端操做的狀況--咱們不多跨級別的修改節點,一般是修改節點的屬性、調整子節點的順序、添加子節點等。當比較虛擬DOM樹的時候,若是發現節點已經不存在,則該節點及其子節點會被徹底刪除掉,不會用於進一步的比較。這樣只須要對樹進行一次遍歷,便能完成整個DOM樹的比較。
虛擬DOM在比較時只比較同層次節點,其複雜度下降到了O(n). 並且比較時只比較其key和sel是否相同,相同即爲相同節點。
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
複製代碼
例子:下圖節點從左圖變爲右圖
虛擬DOM的作法是
A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
複製代碼
而不是
A.parent.remove(A);
D.append(A);
複製代碼
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
...
const elm = vnode.elm = (oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return; // 都是undefined
...
if (isUndef(vnode.text)) { // 新節點不是textNode
if (isDef(oldCh) && isDef(ch)) {
// 子節點都存在,updateChildren對子節點進行diff
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
// 舊節點沒有子節點,且新節點有子節點。將新節點的子節點添加進來
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 新節點沒有子節點,且舊節點有子節點。 刪除舊節點的子節點
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
// 新舊節點都沒有子節點。更新text
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 新節點是textNode且新舊不一致
api.setTextContent(elm, vnode.text as string);
}
...
}
複製代碼
若是兩個元素相同(key和sel),則判斷其children,過程當中維護四個變量
例以下圖中children由ABCDEF -> ADGCEF,其中假設其sel相同且都設置有key,A的key爲A,B的key爲B,依次類推
循環判斷以下:
爲何維護四個變量?有什麼優點?兩個變量是否能夠?此處留個疑問。
oldStart === newStart,則執行上面3.3. 且oldStartIdx++, newStartIdx++.
oldEnd === newEnd,則執行上面3.3. 且oldEndIdx--, newEndIdx--.
同上,oldEnd === newEnd,則執行上面3.3. 且oldEndIdx--, newEndIdx--.
oldEnd === newStart,將oldEnd插入到oldStart以前,並執行上面3.3. 且oldEndIdx--, newStartIdx++.
首尾元素均不相同!判斷newStart在舊元素中是否存在,存在則移動,不然將新元素插入
oldKeyToIdx = [B, C] // 從oldStartIdx到oldEndIdx的全部元素
G in [B, C] ? NO!
複製代碼
將newStart插入到oldStart以前,並執行上面3.3.且newStartIdx++.
同上。H in [B, C] ? NO! 將newStart插入到oldStart以前,並執行3.3.且newStartIdx++.
新節點遍歷完成。跳出循環,依次刪除B和C。結束
至此,循環遍歷結束。如今回答上面的問題,爲何維護四個變量?有什麼優點?兩個變量是否能夠?兩個變量固然是能夠的,四個變量的優點在於:四個變量能夠更好的應對插入的場景。例如: