Virtual Dom && Diff原理,極簡版

dykily發表於2019-02-28

前言

先介紹一個概念Virtual Dom,我猜大家或多或少都應該知道什麼是Virtual Dom吧,簡單來說就是用js來模擬DOM中的結點。

Virtual Dom

下面就是一個Virtual Dom的結構,包含了標籤名,擁有的屬性,孩子結點,render函式

class Element {
  constructor(tagName, attrs, children) {
    this.tagName  = tagName;
    this.attrs    = attrs || {};
    this.children = children || [];
  }
  render () {
    //這個函式是用來生成真實DOM的,最後會把return的結果新增到頁面中去 
  }
}
複製程式碼

建立一棵個Virtual Dom Tree && 渲染

/**
<ul id="list">
  <li class="a">txt_a</li>
  <li class="a">txt_b</li>
</ul>
**/
//根據上面結構可以用一下方式建立一棵 Virtual Dom Tree
let ul = Element('ul', { id: 'list' }, [
  Element('li', { class: 'a' }, ['txt_a']),
  Element('li', { class: 'b' }, ['txt_b'])
]);//ul 就是一棵個Virtual Dom Tree
let ulDom = ul.render();//生成真實Dom
document.body.appendChild(ulDom);//新增到頁面中
複製程式碼

以上就是Virtual Dom Tree如何被轉換成真實Dom並新增到網頁中的過程,再這個過程中我把render函式給省略,只是為了讓你們先了解原理,具體實現可以以後再深究。我學一個東西的時候,習慣是先把整體原理弄清楚,再去深入學習相關的知識。

Diff 演算法

在介紹Diff演算法之前,再次宣告我只會列舉Diff演算法中會用到的函式,並串聯它們之間的關係並不會給出具體實現的程式碼

介紹

diff演算法是進行虛擬節點Element的對比,並返回一個patchs物件,用來儲存兩個節點不同的地方,最後用patchs記錄的訊息去區域性更新Dom。

兩個樹如果完全比較的話需要時間複雜度為O(n^3),如果對O(n^3)不太清楚的話建議去網上搜尋資料。而在Diff演算法中因為考慮效率的問題,只會對同層級元素比較,時間複雜度則為O(n),說白了就是深度遍歷,並比較同層級的節點。

Virtual Dom && Diff原理,極簡版

Diff只需下面兩句程式碼

  • 判斷兩棵Virtual Dom Tree 差異
  • 把差異更新到真實Dom中去
let patchs = diff(oldTree, newTree);//獲取兩棵Virtual Dom Tree 差異
patch(ulDom, patchs);//找到對應的真實dom,進行部分渲染
複製程式碼

Diff中所用到的函式

//深度遍歷樹,將需要做變更操作的取出來
//區域性更新 DOM
function patch(node,patchs){
    //程式碼略
}

// diff 入口,比較新舊兩棵樹的差異
function diff (oldTree, newTree) {
  let index   = 0
  let patches = {} // 記錄每個節點差異的補丁
  dfs(oldTree, newTree, index, patches)
  return patches;
}
/**
 * dfs 深度遍歷查詢節點差異
 * @param  oldNode - 舊虛擬Dom樹
 * @param  newNode - 新虛擬Dom樹
 * @param  index - 當前所在樹的第幾層
 * @param  patches - 記錄節點差異
 */
function dfs (oldNode, newNode, index, patches){
    let currentPatch = [];//當前層的差異對比
    if (!newNode) {
        //如果節點不存不用處理,listdiff函式會處理被刪除的節點
    }else if (isTxt(oldNode) && isTxt(newNode)) {//isTxt用來判斷是否是文字,為了簡便這邊並沒有宣告
        if (newNode !== oldNode) 
            currentPatch.push({ type: "text", content: newNode })
        //如果發現文字不同,currentPatch會記錄一個差異 
    }else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key){
        //如果發現兩個節點一樣 則去判斷節點是屬性是否一樣,並記錄下來
        let attrsPatches = diffAttrs(oldNode, newNode)
        if (attrsPatches) {//有屬性差異則把差異記錄下來
          currentPatch.push({ type: "attrs", "attrs": attrsPatches })
        }
        // 遞迴遍歷子節點,並對子節點進行diff比較
        diffChildren(oldNode.children, newNode.children, index, patches)
    }else{
        //最後一種情況是,兩個節點完全不一樣,這樣只需要把舊節點之間替換就行
        //把當前差異記錄下來
        currentPatch.push({ type: "replace", node: newNode})
    }
    
    //如果有差異則記錄到當前層去
    if (currentPatch.length) {
        if (patches[index]) {
          patches[index] = patches[index].concat(currentPatch)
        } else {
          patches[index] = currentPatch
        }
     }
}
//判斷兩個節點的屬性差異
function diffAttrs(oldNode, newNode){
    let attrsPatches = {};//記錄差異
    let count = 0;//記錄差異的條數
    
    /**
    程式碼略
    判斷兩個節點的屬性差異的程式碼就略了,
    讓你們知道這裡的程式碼就是判斷兩個節點的屬性有哪些差異,
    如果有差異就記錄在attrsPatches這個物件中,並把它返回
    **/
    if(0 == count){
        return null;
    }else {
       return attrsPatches; 
    }
}
//判斷孩子節點
function diffChildren(oldChild, newChild, index, patches){
    let { changes, list } = listDiff(oldChild, newChild, index, patches);
    if (changes.length) {//如果有差異則記錄到當前層去
        if (patches[index]) {
          patches[index] = patches[index].concat(changes);
        } else {
          patches[index] = changes;
        }
    }
    // 程式碼略
    //遍歷當前陣列
    oldChild && oldChild.forEach((item, i) => {
        // 程式碼略
        let node;// 經過判斷後node節點是同時存在於oldChild 和 newChild中
        //則對節點進行遞迴遍歷 相當於 進入下一層 節點,
        let curIndex;
        dfs(item, node, curIndex, patches);
        // 程式碼略
    })
    
}

//判斷oldNodeList, newNodeList 節點的位置差,主要是為了判斷哪些節點被移動、刪除、新增。
function listDiff(oldNodeList, newNodeList, index){
    let changes = [];//記錄 oldNodeList, newNodeList節點的位置差異,是被移動、刪除、新增
    let list = [];//記錄 oldNodeList,newNodeList 同時存在的節點
    /**
    具體判斷邏輯的程式碼就略了
    **/
    return {changes,list};
}
複製程式碼

如果大家對函式之間的呼叫還不明白的話可以看下面的圖

Virtual Dom && Diff原理,極簡版

最後

Virtual Dom 演算法的實現也就是以下三步

  • 通過 JS 來模擬生成 Virtual Dom Tree
  • 判斷兩個 Tree 的差異
  • 渲染差異

上面省略了很多程式碼,主要是為了讓大家快速瞭解Dom diff 的基本原理和流程,如果想更深入的瞭解,可以在網上查閱相關資料。

相關文章