談談React中Diff算法的策略及實現

一、什麼是Diff算法

  • 傳統Diff:diff算法即差別查找算法;對於Html DOM結構即爲tree的差別查找算法;而對於計算兩顆樹的差別時間複雜度爲O(n^3),顯然成本過高,React不可能採用這種傳統算法;
  • React Diff:前端

    • 以前說過,React採用虛擬DOM技術實現對真實DOM的映射,即React Diff算法的差別查找實質是對兩個JavaScript對象的差別查找;
    • 基於三個策略:
    1. Web UI 中 DOM 節點跨層級的移動操做特別少,能夠忽略不計。(tree diff)
    2. 擁有相同類的兩個組件將會生成類似的樹形結構,擁有不一樣類的兩個組件將會生成不一樣的樹形結(component diff)
    3. 對於同一層級的一組子節點,它們能夠經過惟一 id 進行區分。(element diff)

二、React Diff算法解讀

  • 首先須要明確,只有在React更新階段纔會有Diff算法的運用;
  • React更新機制:

clipboard.png

  • React Diff算法優化策略圖:

clipboard.png

  • React更新階段會對ReactElement類型判斷而進行不一樣的操做;ReactElement類型包含三種即:文本、Dom、組件;
  • 每一個類型的元素更新處理方式:算法

    • 自定義元素的更新,主要是更新render出的節點,作甩手掌櫃交給render出的節點的對應component去管理更新。
    • text節點的更新很簡單,直接更新文案。
    • 瀏覽器基本元素的更新,分爲兩塊:segmentfault

      1. 更新屬性,對比出先後屬性的不一樣,局部更新。而且處理特殊屬性,好比事件綁定。
      2. 子節點的更新,子節點更新主要是找出差別對象,找差別對象的時候也會使用上面的shouldUpdateReactComponent來判斷,若是是能夠直接更新的就會遞歸調用子節點的更新,這樣也會遞歸查找差別對象。不可直接更新的刪除以前的對象或添加新的對象。以後根據差別對象操做dom元素(位置變更,刪除,添加等)。

  • 事實上Diff算法只被調用於React更新階段的DOM元素更新過程;爲何這麼說?

一、 若是爲更新文本類型,內容不一樣就直接更新替換,並不會調用複雜的Diff算法:數組

ReactDOMTextComponent.prototype.receiveComponent(nextText, transaction) {
    //與以前保存的字符串比較
    if (nextText !== this._currentElement) {
      this._currentElement = nextText;
      var nextStringText = '' + nextText;
      if (nextStringText !== this._stringText) {
        this._stringText = nextStringText;
        var commentNodes = this.getHostNode();
        // 替換文本元素
        DOMChildrenOperations.replaceDelimitedText(
          commentNodes[0],
          commentNodes[1],
          nextStringText
        );
      }
    }
  }

二、對於自定義組件元素:瀏覽器

class Tab extends Component {
    constructor(props) {
        super(props);
        this.state = {
            index: 1,
        }
    }
    shouldComponentUpdate() {
        ....
    }
    render() {
        return (
            <div>
                <p>item1</p>
                <p>item1</p>
            </div>
        )
    }
    
}
  • 須要明確的是,何爲組件,能夠說組件只不過是一段Html結構的包裝容器,而且具有管理這段Html結構的狀態等能力;
  • 如上述Tab組件:它的實質內容就是render函數返回的Html結構,而咱們所說的Tab類就是這段Html結構的包裝容器(能夠理解爲一個包裝盒子);
  • 在React渲染機制圖中能夠看到,自定義組件的最後結合React Diff優化策略一(不一樣類的兩個組件具有不一樣的結構)

三、基本元素:dom

ReactDOMComponent.prototype.receiveComponent = function(nextElement, transaction, context) {
    var prevElement = this._currentElement;
    this._currentElement = nextElement;
    this.updateComponent(transaction, prevElement, nextElement, context);
}

ReactDOMComponent.prototype.updateComponent = function(transaction, prevElement, nextElement, context) {
    //須要單獨的更新屬性
    this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
    //再更新子節點
    this._updateDOMChildren(
      lastProps,
      nextProps,
      transaction,
      context
    );

    // ......
}
  • 在this._updateDOMChildren方法內部才調用了diff算法。

三、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:函數

    1. 開發組件時,注意保持DOM結構的穩定;即,儘量少地動態操做DOM結構,尤爲是移動操做。
    2. 當節點數過大或者頁面更新次數過多時,頁面卡頓的現象會比較明顯。
    3. 這時能夠經過 CSS 隱藏或顯示節點,而不是真的移除或添加 DOM 節點。
  • 基於component diff性能

    1. 注意使用 shouldComponentUpdate() 來減小組件沒必要要的更新。
    2. 對於相似的結構應該儘可能封裝成組件,既減小代碼量,又能減小component diff的性能消耗。
  • 基於element diff學習

    1. 對於列表結構,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。
  • 接下來手動實現一個簡單的Diff算法即將更新,敬請期待~~~
「積跬步、行千里」—— 持續更新中~,喜歡留下個贊哦!
相關文章
相關標籤/搜索