Vue在2.0版本引入了虛擬DOM。其虛擬DOM算法是基於snabbdom算法所作的修改。參看https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js註釋部分。要想了解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操做, 參考文檔
實現虛擬DOM包含如下三個步驟git
虛擬DOM對象包含如下屬性:github
參考https://github.com/snabbdom/snabbdom/blob/master/src/tovnode.ts算法
給定任意兩棵樹,找到最少的轉換步驟。可是標準的的Diff算法複雜度須要O(n^3). segmentfault
這顯然沒法知足性能的要求,考慮到前端操做的狀況--咱們不多跨級別的修改節點,一般是修改節點的屬性、調整子節點的順序、添加子節點等。當比較虛擬DOM樹的時候,若是發現節點已經不存在,則該節點及其子節點會被徹底刪除掉,不會用於進一步的比較。這樣只須要對樹進行一次遍歷,便能完成整個DOM樹的比較。api
虛擬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,依次類推
循環判斷以下:
參看源碼https://github.com/snabbdom/snabbdom/blob/master/src/snabbdom.ts#L179
爲何維護四個變量?有什麼優點?兩個變量是否能夠?此處留個疑問。
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。結束
至此,循環遍歷結束。如今回答上面的問題,爲何維護四個變量?有什麼優點?兩個變量是否能夠?兩個變量固然是能夠的,四個變量的優點在於:四個變量能夠更好的應對插入的場景。例如: