React源碼分析 - Diff算法

在《React源碼分析 - 組件更新與事務》中的流程圖的最後:javascript

diff update

藍色框框的部分分別是Diff算法的核心代碼updateChildren以及processUpdates,經過Diff算法獲取了組件更新的updates隊列以後一次性進行更新。java

Diff算法的代碼(先彆着急下面會具體解釋算法的主要步驟):node

_updateChildren: function (nextNestedChildrenElements, transaction, context) {
      var prevChildren = this._renderedChildren;
      var removedNodes = {};
      var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, removedNodes, transaction, context);
      if (!nextChildren && !prevChildren) {
        return;
      }
      var updates = null;
      var name;
      var lastIndex = 0;
      var nextIndex = 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) {
          updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
          lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          prevChild._mountIndex = nextIndex;
        } else {
          if (prevChild) {
            lastIndex = Math.max(prevChild._mountIndex, lastIndex);
          }
          updates = enqueue(updates, this._mountChildAtIndex(nextChild, lastPlacedNode, nextIndex, transaction, context));
        }
        nextIndex++;
        lastPlacedNode = ReactReconciler.getNativeNode(nextChild);
      }
      for (name in removedNodes) {
        if (removedNodes.hasOwnProperty(name)) {
          updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
        }
      }
      if (updates) {
        processQueue(this, updates);
      }
      this._renderedChildren = nextChildren;
    }
複製代碼

《深刻React技術棧》這本書對Diff算法的解釋比較好。其實只要記住幾個原則以及在具體的計算updates隊列的時候的算法優化的點就行了。react

傳統的diff算法的複雜度是O(n^3),想要具體的瞭解能夠去看"A Survey on Tree Edit Distance and Related Problems"算法

這種複雜度在實際中應用會爆炸的,雖然如今的電腦的CPU很強,但一個頁面也不能這樣任性~。數組

對此React的作法是給出合理的假設和方法來讓整個diff過程合理簡化。瀏覽器

  • DOM節點跨層級的移動操做的場景是不多見的,能夠忽略不計。(合理,能夠經過組件的設計來儘可能保證DOM結構的穩定,必要時能夠經過CSS的方法來進行DOM在展現上的調整,由於建立、刪除以及移動DOM的操做是能少則少,瀏覽器的每一個DOM節點都是一個大對象,有着不少的方法和屬性
  • 同一類的兩個組件將會生成類似的樹形結構,不一樣類的兩個組件將會生成不一樣的樹形結構。(合理,自己組件就有提升頁面的可複用性的做用,也就是將結構功能相似的頁面結構(或者說類似的DOM樹形結構)抽象成一類組件,因此合理的組件抽象就應該知足這條假設)
  • 對於同一層級的一組節點能夠經過設置惟一的key來進行區分,從而作到diff的進一步優化。(這個不算是一個假設而是一個提升性能的方法)
  • 對於同一類的兩個組件,有可能其Virtual Dom是沒有任何變化的。所以React容許開發者經過shouldComponentUpdate()來判斷組件是否須要進行diff算法分析。(合理,開發者自己對頁面的理解來進一步進行diff的優化,固然這有可能會由於開發者錯誤的使用shouldComponentUpdate()判斷錯誤了是否須要更新,從而獲得了錯誤的結果.....可是這怪sei ???,寫bug了還不老實)

基於上面的幾條,在具體的Diff過程當中React只進行分層比較,新舊的樹之間只比較同一個層次的節點。節點的操做分爲3種:插入、移動和刪除。dom

節點移動操做判斷的過程,引用《深刻React技術棧》中的話:ide

首先對新集合的節點進行循環遍歷,for (name in nextChildren),經過惟一 key 能夠判斷新老集合中是否存在相同的節點,if (prevChild === nextChild),若是存在相同節點,則進行移動操做,但在移動前須要將當前節點在老集合中的位置與 lastIndex 進行比較,if (child._mountIndex < lastIndex),則進行節點移動操做,不然不執行該操做。這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置),若是新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做,只有當訪問的節點比 lastIndex 小時,才須要進行移動操做。源碼分析

須要注意的是」這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置),若是新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其餘節點的位置,所以不用添加到差別隊列中,即不執行移動操做「這句話。意思是若是一個節點在舊集合中的位置已經在你以前進行判斷的最後一個節點的背後,那麼這個節點已經在被diff過的節點的後面了和以前的diff過的節點在順序上就已是正確的了,不須要移動了,反之的節點須要被移動。

另外須要知道的是若是沒有給key賦值,React會默認使用的是遍歷過程當中的 index 值。這裏的index值指的是節點遍歷的順序號,效果等同於有些小夥伴用列表數組的index來當作key。這樣實際上是很差的,由於節點的key和節點的位置有關係和節點自己不要緊,也就是若是我一個列表有10個節點,按照遍歷的順序key爲1到10,而後我在列表的最開始增長了一個節點,這個時候按照列表遍歷的順序來設置key,則原來的10個節點的key都變了,並且新舊節點的key錯誤的對上了,要知道key在React中時對一個組件的身份識別的標示,錯誤或者重複的key會形成React錯誤的結果......so.......key須要是一個和節點自己有聯繫的惟一標示。

react的做者之一Paul O’Shannessy有提到:

Key is not really about performance, it’s more about identity (which in turn leads to better performance). Randomly assigned and changing values do not form an identity

你可能會問,上面的diff算法的源碼部分沒看到key啊,恩,其實每一個component的key會變成nextChildren&prevChildren對象中的name對應的value是component,另外在_reconcilerUpdateChildren中的shouldUpdateReactComponent組件的key也有使用到。

對於新增和刪除節點的操做簡單來講:

  • 新增節點就是建立新的節點放在順序遍歷到的位置上。
  • 刪除節點則是在該層次遍歷結束後,對舊集合進行循環遍歷,判斷是否在新集合中沒有,沒有的話,則刪除節點。

固然上面說的移動、新增和刪除節點的操做,不是立刻執行的,而是收集到updates數組中,而後用processUpdates方法一次性進行具體的DOM的的更新。

processUpdates: function (parentNode, updates) {
    for (var k = 0; k < updates.length; k++) {
      var update = updates[k];
      switch (update.type) {
        case ReactMultiChildUpdateTypes.INSERT_MARKUP:
          insertLazyTreeChildAt(parentNode, update.content, getNodeAfter(parentNode, update.afterNode));
          break;
        case ReactMultiChildUpdateTypes.MOVE_EXISTING:
          moveChild(parentNode, update.fromNode, getNodeAfter(parentNode, update.afterNode));
          break;
        case ReactMultiChildUpdateTypes.SET_MARKUP:
          setInnerHTML(parentNode, update.content);
          break;
        case ReactMultiChildUpdateTypes.TEXT_CONTENT:
          setTextContent(parentNode, update.content);
          break;
        case ReactMultiChildUpdateTypes.REMOVE_NODE:
          removeChild(parentNode, update.fromNode);
          break;
      }
    }
  }
複製代碼

其中的節點的具體的操做就是到具體的瀏覽器的DOM的節點的操做了,舉個栗子。

function insertLazyTreeChildAt(parentNode, childTree, referenceNode) {
  DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode);
}

var insertTreeBefore = createMicrosoftUnsafeLocalFunction(function (parentNode, tree, referenceNode) {
  if (tree.node.nodeType === 11) {
    insertTreeChildren(tree);
    parentNode.insertBefore(tree.node, referenceNode);
  } else {
    parentNode.insertBefore(tree.node, referenceNode);
    insertTreeChildren(tree);
  }
});
複製代碼

Node.insertBefore()就是瀏覽器DOM操做的API了。

想要跟着具體的Diff的過程來理解的話,推薦單步調試或者看《深刻React技術棧》中的栗子,這裏我就不畫了.....畫圖很累的.....網上也很是多相似的搜一下就行了。

本文對key的具體的使用的部分有待進一步深刻。【TBD】

參考資料:

相關文章
相關標籤/搜索