瞭解虛擬DOM

背景

Vue在2.0版本引入了虛擬DOM。其了虛擬DOM算法是基於snabbdom算法所作的修改。參看github.com/vuejs/vue/b…註釋部分。要想了解Vue,必須瞭解虛擬DOM,本篇文章主要介紹了什麼是虛擬DOM,爲何用虛擬DOM以及其具體實現。html

1、什麼是虛擬DOM

用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: '橙子'}
    ]
}
複製代碼

2、爲何要用虛擬DOM

由於對DOM的直接操做是很是慢並且低效的。瀏覽器的渲染流程包括解析html以構建dom樹->構建render樹->佈局render樹->繪製render樹,而每一次DOM改變從構建render樹到佈局到渲染都要重來。參考文檔node

而虛擬DOM的優點就是:1.開發者再也不關心DOM而只關心數據,提高開發效率。2.保證最小化的DOM操做,使執行效率獲得提高。react

虛擬DOM的優點並不在於它操做DOM比較快,而是可以經過虛擬DOM的比較,最小化真實DOM操做,參考文檔git

3、虛擬DOM的實現

實現虛擬DOM包含如下三個步驟github

  1. 用JavaScript模擬DOM樹造成虛擬DOM樹算法

  2. 當組件狀態發生更新時,比較新舊虛擬DOM樹segmentfault

  3. 將差別應用到真正的DOM上 api

3.1 用JavaScript模擬DOM樹造成虛擬DOM樹

虛擬DOM對象包含如下屬性:

  • sel:選擇器
  • data:綁定的數據(attribute/props/eventlistner/class/dataset/hook)
  • children:子節點數組
  • text:當前text節點內容
  • elm: 對真實dom element的引用
  • key:用於優化DOM操做

參考github.com/snabbdom/sn…

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上
  1. 若是舊節點不在,則將新節點插入
  2. 若是新節點不存在,則將舊節點刪除
  3. 若是新舊相同(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);
    }
    ...
  }
複製代碼

4、舉個例子

若是兩個元素相同(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--.

第四步

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

第七步

新節點遍歷完成。跳出循環,依次刪除B和C。結束

至此,循環遍歷結束。如今回答上面的問題,爲何維護四個變量?有什麼優點?兩個變量是否能夠?兩個變量固然是能夠的,四個變量的優點在於:四個變量能夠更好的應對插入的場景。例如:

  • ABCDEF -> GABCDEF的場景
  • ABCDEF -> BCDEFA的場景

參考文檔

相關文章
相關標籤/搜索