【Vue】Diff算法解析

概念

DIFF 算法即兩顆虛擬 DOM 樹做 DIFF,比較兩顆樹的差異性,只更新產(chǎn)生變化的節(jié)點(diǎn),渲染更新后的節(jié)點(diǎn)到真實(shí)的 DOM,以節(jié)約資源。

用戶每次修改數(shù)據(jù)后都會引起頁面元素重排和重繪,渲染真實(shí)的 DOM 開銷較大,非常消耗性能。相對于真實(shí) DOM 對象,js對象(即虛擬DOM)處理起來更快,而且更簡單。 通過 DIFF 算法對比新舊 virtual DOM 之間的差異,可以批量的、最小化的執(zhí)行 DOM 操作,從而提高性能。

我們先根據(jù)真實(shí) DOM 生成一顆 virtual DOM,當(dāng) virtual DOM 某個節(jié)點(diǎn)的數(shù)據(jù)改變后會生成一個新的Vnode,然后 Vnode 和 oldVnode 作對比,發(fā)現(xiàn)有不一樣的地方就直接修改在真實(shí)的 DOM 上,然后使 oldVnode 的值更新為 Vnode。

DIFF 的過程就是調(diào)用名為 patch 的函數(shù),比較新舊節(jié)點(diǎn),一邊比較一邊給真實(shí)的 DOM 打補(bǔ)丁。

比較的方式

diff時間復(fù)雜度分析:
常規(guī):O(n^3) 遍歷樹1; 遍歷樹2; 排序; 1000個節(jié)點(diǎn),十億的量級。
優(yōu)化:O(n) 只比較同一層級 ;tag不相同,直接刪掉重建; 通過key來標(biāo)識區(qū)分相同節(jié)點(diǎn)。

在采取diff算法比較新舊節(jié)點(diǎn)的時候,比較只會在同層級進(jìn)行, 不會跨層級比較。若對 DOM 樹進(jìn)行跨層比較,即進(jìn)行完全遍歷,算法的時間復(fù)雜度與節(jié)點(diǎn)個數(shù)呈指數(shù)級增長。
同時Web UI 中 DOM 節(jié)點(diǎn)跨層級的移動操作特別少,可以忽略不計,所以diff的算法只做同層比較,若 Vnode 做了跨層移動,則直接刪除 oldVnode, 再新建一個 new Vnode。

DIFF流程圖

當(dāng)用戶操作數(shù)據(jù)發(fā)生改變的時候,set方法會讓調(diào)用Dep.notify通知所有訂閱者Watcher,訂閱者就會調(diào)用patch給真實(shí)的DOM打補(bǔ)丁,更新相應(yīng)的視圖。


image.png

具體分析

在 Vue 中,diff 算法主要聚焦于 vDOM 更新過程中的三個關(guān)鍵函數(shù),分別是 patch、patchVNode、updateChildren。

patch

patch:比對兩個虛擬dom時會有三種操作:刪除、新增、更新


image.png

入?yún)榕f節(jié)點(diǎn)和新節(jié)點(diǎn),首先判斷新老節(jié)點(diǎn)是否為同一個節(jié)點(diǎn),判斷方法即比較他們的 key 值、tag 值(標(biāo)簽類型)是否相同,若為同一個節(jié)點(diǎn)則執(zhí)行 patchVnode 方法進(jìn)行打補(bǔ)丁更新。

以下的代碼是 Vue 源碼中相關(guān)函數(shù)的精簡偽代碼,api 指的是原生的DOM操作函數(shù)集合。

function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el // 當(dāng)前oldVnode對應(yīng)的真實(shí)元素節(jié)點(diǎn)
        let parentEle = api.parentNode(oEl)  // 父元素
        createEle(vnode)  // 根據(jù)Vnode生成新元素
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 將新元素添加進(jìn)父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的舊元素節(jié)點(diǎn)
            oldVnode = null
        }
    }
    // some code 
    return vnode
}
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 標(biāo)簽名
    a.isComment === b.isComment &&  // 是否為注釋節(jié)點(diǎn)
    // 是否都定義了data,data包含一些具體信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 當(dāng)標(biāo)簽是<input>的時候,type必須相同
  )
}

patchVnode

patchVnode:比較兩個VNode三種類型操作 屬性更新、文本更新、子節(jié)點(diǎn)更新


image.png
patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

updateChildren

updateChildren:用一種較高效的方式對比新舊兩個VNode的children,得出最小操作補(bǔ)丁。四指針雙循環(huán)執(zhí)行方式。


image.png
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0;
    var newStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    var canMove = !removeOnly;

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh);
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        } else {
          vnodeToMove = oldCh[idxInOld];
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined;
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

四指針雙循環(huán):
保證 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx 前提下,執(zhí)行如下循環(huán)算法:

  1. oldStartVNode 和 newStartVnode 滿足 sameVnode 的條件,進(jìn)行 patchVnode, 兩個頭指針右移。
  2. oldEndVnode 和 newEndVnode 滿足 sameVnode,進(jìn)行 patchVnode,兩個尾指針左移。
  3. oldStartVnode 和 newEndVnode 滿足 sameVnode, 進(jìn)行 patchVnode,同時把頭元素插入到 children nodes 尾部。++oldStartIdx,--newEndIdx。
  4. sameVnode(oldEndVnode, newStartVnode)。--oldStartIdx,++newEndIdx。

不符合以上四種情況,則直接尋找 oldCh 中是否存在和 newStartVnode 相同的節(jié)點(diǎn),并移動到最前。
找不到和 newStartVnode 一致的節(jié)點(diǎn),則調(diào)用 createElm 創(chuàng)建一個新的 DOM 節(jié)點(diǎn)。

循環(huán)結(jié)束后,處理剩余節(jié)點(diǎn),
若 oldStartIdx > oldEndIdx 此時舊的 Vnode 節(jié)點(diǎn)已經(jīng)遍歷結(jié)束,但是新的節(jié)點(diǎn)還沒遍歷結(jié)束,則將剩余的 Vnode(>=0) 按照對應(yīng)的下標(biāo)插入到真實(shí)的DOM中去。

若 newStartIdx > newEndIdx, 此時新的 Vnode 已經(jīng)遍歷完成,但是老的節(jié)點(diǎn)還未遍歷結(jié)束,說明需要在真實(shí)的 DOM 中把多余的老的Vnode節(jié)點(diǎn)刪除。

圖解直接看文檔吧,配合源碼食用更佳:https://www.cnblogs.com/lilicat/p/13448827.html

參考文檔:

https://www.cnblogs.com/lilicat/p/13448784.html

https://www.cnblogs.com/wind-lanyan/p/9061684.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容