Diff算法


Diff算法

什么是Diff算法?

diff算法作為Virtual DOM的加速器,其算法的改進優化是React整個界面渲染的基礎和性能的保障,同時也是React源碼中最神秘的,最不可思議的部分

傳統Diff:

計算一棵樹形結構轉換為另一棵樹形結構需要最少步驟,如果使用傳統的diff算法通過循環遞歸遍歷節點進行對比,其復雜度要達到O(n^3),其中n是節點總數,效率十分低下,假設我們要展示1000個節點,那么我們就要依次執行上十億次的比較。

下面附上一則簡單的傳統diff算法:

let result = [];
// 比較葉子節點
const diffLeafs = function (beforeLeaf, afterLeaf) {
    // 獲取較大節點樹的長度
    let count = Math.max(beforeLeaf.children.length, afterLeaf.children.length);
    // 循環遍歷
    for (let i = 0; i < count; i++) {
        const beforeTag = beforeLeaf.children[i];
        const afterTag = afterLeaf.children[i];
        // 添加 afterTag 節點
        if (beforeTag === undefined) {
            result.push({ type: "add", element: afterTag });
            // 刪除 beforeTag 節點
        } else if (afterTag === undefined) {
            result.push({ type: "remove", element: beforeTag });
            // 節點名改變時,刪除 beforeTag 節點,添加 afterTag 節點
        } else if (beforeTag.tagName !== afterTag.tagName) {
            result.push({ type: "remove", element: beforeTag });
            result.push({ type: "add", element: afterTag });
            // 節點不變而內容改變時,改變節點
        } else if (beforeTag.innerHTML !== afterTag.innerHTML) {
            if (beforeTag.children.length === 0) {
                result.push({
                    type: "changed",
                    beforeElement: beforeTag,
                    afterElement: afterTag,
                    html: afterTag.innerHTML
                });
            } else {
                // 遞歸比較
                diffLeafs(beforeTag, afterTag);
            }
        }
    }
    return result;
}

 

 

React Diff算法優化策略圖

在這里插入圖片描述

  • React更新階段會對ReactElement類型判斷而進行不同的操作;ReactElement類型包含三種即:文本、Dom、組件;
  • 每個類型的元素更新處理方式:
    • 自定義元素的更新,主要是更新render出的節點,做甩手掌櫃交給render出的節點的對應component去管理更新。
    • text節點的更新很簡單,直接更新文案。
    • 瀏覽器基本元素的更新,分為兩塊:
      • 更新屬性,對比出前后屬性的不同,局部更新。並且處理特殊屬性,比如事件綁定。
      • 子節點的更新,子節點更新主要是找出差異對象,找差異對象的時候也會使用上面的shouldUpdateReactComponent來判斷,如果是可以直接更新的就會遞歸調用子節點的更新,這樣也會遞歸查找差異對象。不可直接更新的刪除之前的對象或添加新的對象。之后根據差異對象操作dom元素(位置變動,刪除,添加等)

React中Diff算法的實現

React Diff:

之前說過,React采用虛擬DOM技術實現對真實DOM的映射,即React Diff算法的差異查找實質是對兩個JavaScript對象的差異查找;

基於三個策略:

1.Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計。(tree diff)
2.擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結。(component diff)
3.對於同一層級的一組子節點,它們可以通過唯一ID進行區分。(element diff)
  • 1
  • 2
  • 3
對於以上三個策略,react分別對tree diff,component diff,element diff進行算法優化。
1.tree diff

基於策略一,WebUI中DOM節點跨層級的移動操作少的可以忽略不計,React對Virtual DOM樹進行層級控制,只會對相同層級的DOM節點進行比較,即同一個父元素下的所有子節點,當發現節點已經不存在了,則會刪除掉該節點下所有的子節點,不會再進行比較。這樣只需要對DOM樹進行一次遍歷,就可以完成整個樹的比較。復雜度變為O(n);

疑問:當我們的DOM節點進行跨層級操作時,diff會有怎么樣的表現呢?

如下圖所示,A節點及其子節點被整個移動到D節點下面去,由於React只會簡單的考慮同級節點的位置變換,而對於不同層級的節點,只有創建和刪除操作,所以當根節點發現A節點消失了,就會刪除A節點及其子節點,當D發現多了一個子節點A,就會創建新的A作為其子節點。
此時,diff的執行情況是:

createA-->createB-->createC-->deleteA

在這里插入圖片描述

Tree DIFF是對樹的每一層進行遍歷,如果某組件不存在了,則會直接銷毀。如圖所示,左邊是舊屬,右邊是新屬,第一層是R組件,一模一樣,不會發生變化;第二層進入Component DIFF,同一類型組件繼續比較下去,發現A組件沒有,所以直接刪掉A、B、C組件;繼續第三層,重新創建A、B、C組件。

由此可以發現,當出現節點跨層級移動時,並不會出現想象中的移動操作,而是會進行刪除,重新創建的動作,這是一種很影響React性能的操作。因此官方也不建議進行DOM節點跨層級的操作。

2.componnet diff

React是基於組件構建應用的,對於組件間的比較所采用的策略也是非常簡潔和高效的。

  • 如果是同一個類型的組件,則按照原策略進行Virtual DOM比較。

  • 如果不是同一類型的組件,則將其判斷為dirty component,從而替換整個組價下的所有子節點。

  • 如果是同一個類型的組件,有可能經過一輪Virtual DOM比較下來,並沒有發生變化。如果我們能夠提前確切知道這一點,那么就可以省下大量的diff運算時間。因此,React允許用戶通過shouldComponentUpdate()來判斷該組件是否需要進行diff算法分析。

如下圖所示,當組件D變為組件G時,即使這兩個組件結構相似,一旦React判斷D和G是不用類型的組件,就不會比較兩者的結構,而是直接刪除組件D,重新創建組件G及其子節點。雖然當兩個組件是不同類型但結構相似時,進行diff算法分析會影響性能,但是畢竟不同類型的組件存在相似DOM樹的情況在實際開發過程中很少出現,因此這種極端因素很難在實際開發過程中造成重大影響。

在這里插入圖片描述

3.element diff

當節點屬於同一層級時,diff提供了3種節點操作,分別為INSERT_MARKUP(插入),MOVE_EXISTING(移動),REMOVE_NODE(刪除)。

  • INSERT_MARKUP:新的組件類型不在舊集合中,即全新的節點,需要對新節點進行插入操作。
  • MOVE_EXISTING:舊集合中有新組件類型,且element是可更新的類型,這時候就需要做移動操作,可以復用以前的DOM節點。
  • REMOVE_NODE:舊組件類型,在新集合里也有,但對應的element不同則不能直接復用和更新,需要執行刪除操作,或者舊組件不在新集合里的,也需要執行刪除操作。

在這里插入圖片描述

Element DIFF緊接着以上統一類型組件繼續比較下去,常見類型就是列表。同一個列表由舊變新有三種行為,插入、移動和刪除,它的比較策略是對於每一個列表指定key,先將所有列表遍歷一遍,確定要新增和刪除的,再確定需要移動的。如圖所示,第一步將D刪掉,第二步增加E,再次執行時A和B只需要移動位置即可。

React中Diff算法實現的代碼

_updateChildren: function(nextNestedChildrenElements, transaction, context) {
    var prevChildren = this._renderedChildren;
    var removedNodes = {};
    var mountImages = [];
    // 獲取新的子元素數組
    var nextChildren = this._reconcilerUpdateChildren(
      prevChildren,
      nextNestedChildrenElements,
      mountImages,
      removedNodes,
      transaction,
      context
    );
    if (!nextChildren && !prevChildren) {
      return;
    }
    var updates = null;
    var name;
    var nextIndex = 0;
    var lastIndex = 0;
    var nextMountIndex = 0;
    var lastPlacedNode = null;
    for (name in nextChildren) {
      if (!nextChildren.hasOwnProperty(name)) {
        continue;
      }
      var prevChild = prevChildren && prevChildren[name];
      var nextChild = nextChildren[name];
      if (prevChild === nextChild) {
        // 同一個引用,說明是使用的同一個component,所以我們需要做移動的操作
        // 移動已有的子節點
        // NOTICE:這里根據nextIndex, lastIndex決定是否移動
        updates = enqueue(
          updates,
          this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)
        );
        // 更新lastIndex
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        // 更新component的.mountIndex屬性
        prevChild._mountIndex = nextIndex;
      } else {
        if (prevChild) {
          // 更新lastIndex
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
        }

        // 添加新的子節點在指定的位置上
        updates = enqueue(
          updates,
          this._mountChildAtIndex(
            nextChild,
            mountImages[nextMountIndex],
            lastPlacedNode,
            nextIndex,
            transaction,
            context
          )
        );
        nextMountIndex++;
      }
      // 更新nextIndex
      nextIndex++;
      lastPlacedNode = ReactReconciler.getHostNode(nextChild);
    }
    // 移除掉不存在的舊子節點,和舊子節點和新子節點不同的舊子節點
    for (name in removedNodes) {
      if (removedNodes.hasOwnProperty(name)) {
        updates = enqueue(
          updates,
          this._unmountChild(prevChildren[name], removedNodes[name])
        );
      }
    }
  }

 

 

基於中Diff的開發建議

基於tree diff:

  • 開發組件時,注意保持DOM結構的穩定;即,盡可能少地動態操作DOM結構,尤其是移動操作。
  • 當節點數過大或者頁面更新次數過多時,頁面卡頓的現象會比較明顯。
  • 這時可以通過 CSS 隱藏或顯示節點,而不是真的移除或添加 DOM 節點。

基於component diff:

  • 注意使用 shouldComponentUpdate() 來減少組件不必要的更新。
  • 對於類似的結構應該盡量封裝成組件,既減少代碼量,又能減少component diff的性能消耗。

基於element diff:

  • 對於列表結構,盡量減少類似將最后一個節點移動到列表首部的操作,當節點數量過大或更新操作過於頻繁時,在一定程度上會影響 React 的渲染性能。

轉載:https://blog.csdn.net/weixin_44369568/article/details/91488905


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM