瞭解虛擬DOM
Vue在2.0版本引入了虛擬DOM。其了虛擬DOM演算法是基於snabbdom演算法所做的修改。參看 ofollow,noindex">github.com/vuejs/vue/b… 註釋部分。要想了解Vue,必須瞭解虛擬DOM,本篇文章主要介紹了什麼是虛擬DOM,為什麼用虛擬DOM以及其實現節點
一、什麼是虛擬DOM
用JavaScript模擬DOM樹形成虛擬DOM樹,如下面的html結構
<ul style="color:#000"> <li>蘋果</li> <li>香蕉</li> <li>橙子</li> </ul> 複製程式碼
可以使用如下JS表示
{ sel: 'ul', data: { style: {color: '#000'}}, // 節點屬性及繫結事件等 children: [ // 子節點 {sel: 'li', text: '蘋果'}, {sel: 'li', text: '香蕉'}, {sel: 'li', text: '橙子'} ] } 複製程式碼
二、為什麼要用虛擬DOM
因為對DOM的直接操作是非常慢而且低效的。瀏覽器的渲染流程包括解析html以構建dom樹->構建render樹->佈局render樹->繪製render樹,而每一次DOM改變從構建render樹到佈局到渲染都要重來。參考文件
而虛擬DOM的優勢就是:1.開發者不再關心DOM而只關心資料,提升開發效率。2.保證最小化的DOM操作,使執行效率得到提升。
虛擬DOM的優勢並不在於它操作DOM比較快,而是能夠通過虛擬DOM的比較,最小化真實DOM操作,參考文件
三、虛擬DOM的實現
實現虛擬DOM包含以下三個步驟
-
用JavaScript模擬DOM樹形成虛擬DOM樹
-
當元件狀態發生更新時,比較新舊虛擬DOM樹
-
將差異應用到真正的DOM上
3.1 用JavaScript模擬DOM樹形成虛擬DOM樹
虛擬DOM物件包含以下屬性:
- sel:選擇器
- data:繫結的資料(attribute/props/eventlistner/class/dataset/hook)
- children:子節點陣列
- text:當前text節點內容
- elm: 對真實dom element的引用
- key:用於優化DOM操作
3.2 當元件狀態發生更新時,比較新舊虛擬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); 複製程式碼
3.3 將差異應用到真正的DOM上
- 如果舊節點不在,則將新節點插入
- 如果新節點不存在,則將舊節點刪除
- 如果新舊相同(key和sel相同):
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,過程中維護四個變數
- oldStartIdx => 舊頭索引
- oldEndIdx => 舊尾索引
- newStartIdx => 新頭索引
- newEndIdx => 新尾索引
例如下圖中children由ABCDEF -> ADGCEF, 其中假設其sel相同且都設定有key ,A的key為A,B的key為B,依次類推

迴圈判斷如下:
- step1:比較首元素(oldStart/newStart),相同則後移繼續,否則step2
- step2:比較尾元素(oldEnd/newEnd) ,相同則前移繼續,否則step3
- step3:比較首尾元素(oldStart/newEnd) ,相同則移動元素並繼續,否則step4
- step4:比較尾首元素(oldEnd/newStart) ,相同則移動元素並繼續,否則step5
- step5:判斷newStart在舊節點中是否存在,存在則移動,否則新增
- 最後:刪除多餘的舊節點或插入多餘的新節點
參看原始碼 github.com/snabbdom/sn…
為什麼維護四個變數?有什麼優勢?兩個變數是否可以?此處留個疑問。
第一步


oldStart === newStart,則執行上面3.3. 且oldStartIdx++, newStartIdx++.
第二步


oldEnd === newEnd,則執行上面3.3. 且oldEndIdx--, newEndIdx--.
第三步

同上,oldEnd === newEnd,則執行上面3.3. 且oldEndIdx--, newEndIdx--.
第四步


- oldStart !== newStart
- oldEnd !== newEnd
- oldStart !== newEnd
- oldEnd === newStart
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++.