日常抄書之React中Diff算法思路

1. 前言

diff並非React首創,只是React針對diff算法進行了優化。在React中diff算法和Virtual Dom的完美結合可以讓我們不需要關心頁面的變化導致的性能問題。因爲diff會幫助我們計算出Virtual Dom變化的部分,然後只對該部分進行原生DOM操作,而非重新渲染整個頁面,從而保證每次操作更新後頁面的高效渲染。

2. 詳解diff

React將Virtual Dom轉爲真實DOM樹的最少操作的過程叫做調和(reconciliation)。diff便是調和的具體實現。

3. diff策略

  • 策略一: Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計。
  • 策略二: 擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構。
  • 策略三: 對於同一層級的一組節點,它們可以通過唯一id進行區分。

基於以上策略,React分別對tree diff、component diff、element diff進行算法優化。

4. tree diff 對於樹的比較

基於策略一( Web UI中DOM節點跨層級的移動操作特別少,可以忽略不計)。React對樹的算法進行簡潔明瞭的優化,既對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。

既然DOM節點跨層級的移動操作可以忽略不計,針對這一現象,React通過updateDepth對Virtual DOM樹進行層級控制,只會對相同層級的DOM進行比較,即同一個父節點下的所有子節點,當發現節點已經不存在時,則刪除該節點以及其子節點,不會用於進一步比較,這樣只需對樹進行一次遍歷,便能夠完成整個DOM樹比較。

updateChildren: function(nextNestedChildrenElements, transaction, context) {
    updateDepth ++
    var errorThrown = true
    try {
        this._updateChildren(nextNestedChildrenElements, transaction, context)
    } finally {
        updateDepth --
        if(!updateDepth) {
            if(errorThrown) {
                clearQueue()
            }else {
                processQueue()
            }
        }
    }
}

如下出現DOM節點跨層級移動操作,A節點整個被移動到了D節點下,由於React只會簡單的考慮同層級節點的位置變化,對於不同層級的節點只有創建和刪除。當根節點發現子節點中A消失,就會直接銷燬A;當D發現多了一個子節點A,則會創建新的A和其子節點,此時diff執行情況:createA--->createB--->crateC--->createA:


可以發現跨層級移動時,會出現重新創建整個樹的操作,這種比較影響性能,所以不推薦進行DOM節點跨節點操作。

5. component diff 對於組件的比較

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

  • 如果是同一類型的組件,按照原策略繼續比較Virtual DOM樹即可。
  • 如果不是同一類型組件,則將該組件判斷爲dirtycomponent,從而替換整個組件下的所有子組件。
  • 對於同一類型的組件,有可能其Virtual DOM沒有任何變化,如果能夠確切知道這點,那麼就可以節省大量的diff運算時間。因此React 允許用戶通過shouldComponentUpdate來判斷該組件是否需要進行diff算法分析。

6. element diff

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

  • INSERT_MARKUP:新的組件類型不在舊集合裏,即全新的節點,需要對新節點執行插入操作。
  • MOVE_EXISTING:舊集合中有新組件類型,且element是可更新的類型,generateComponentChildren已調用receiveComponent,這種情況下prevChild=nextChild,就需要做移動操作,可以複用以前的DOM節點。
  • REMOVE_NODE:舊組件類型,在新集合裏也有,但對應的element不同則不能直接複用和更新,需要執行刪除操作,或者舊組件不在新集合裏,也需要執行刪除操作。

相關代碼如下:

function makeInsertMarkup(markup, afterNode) {
    return {
        type: ReactMultiChildUpdateTypes.INSERT_MARKUP,
        content: markup,
        fromIndex: null,
        fromNode: null,
        toIndexL toIndex,
        afterNode: afterNode
    }
}

function makeMove(child, afterNode, toIndex) {
    return {
        type: ReactMultiChildUpdateTypes.MOVE)EXISTING,
        content: null,
        fromIndex: child._mountIndex,
        fromNode: ReactReconciler.getNativeNode(child),
        toIndex: toIndex,
        afterNode: afterNode
    }
}

function makeRemove(child, node) {
    return {
        type: ReactMultiChildUpdateTypes.REMOVE_NODE,
        content: null,
        fromIndex: child._mountIndex,
        fromNode: node,
        toIndex: null,
        afterNode: null
    }
}

如下圖:舊集合中包含節點A,B,C,D。更新後集閤中包含節點B,A,D,C。此時新舊集合進行diff差異化對比,發現B!=A,則創建並插入B至新集合,刪除就集合A。以此類推,創建並插入A,D,C。刪除B,C,D。

React發現這類操作繁瑣冗餘,因爲這些都是相同的節點,只是由於位置發生變化,導致需要進行煩雜低效的刪除,創建操作,其實只要對這些節點進行位置移動即可。

針對這一現象,React提出優化策略:允許開發者對同一層級的同組子節點,添加唯一的key進行區別。

新舊集合所包含的節點如下圖所示,進行diff差異化對比後,通過key發現新舊集合中的節點都是相同的節點,因此無需進行節點的刪除和創建,只需要將舊集合中節點的位置進行移動,更爲新集合中節點的位置,此時React給出的diff結果爲:B,D不做任何操作,A,C進行移動即可。

源碼分析一波:
首先,對新集合中的節點進行循環遍歷,通過唯一的key判斷新舊集合中是否存在相同的節點,如果存在相同的節點,則進行移動操作,但在移動前需要將當前節點在舊集合中的位置與lastIndex進行比較。否則不執行該操作。這是一種順序優化手段,lastIndex一直更新,表示訪問過的節點在舊集合中最右邊的位置(即最大的位置)。如果新集合中當前訪問的節點比lastIndex大,說明當前訪問節點在舊集合中就比上一個節點位置靠後,則該節點不會影響其他節點的位置,因此不用添加到差異隊列中,即不執行移動操作

以上面的那個圖爲例子:

  • 從新集合中取得B, 判斷舊集合中是否存在相同節點B,此時發現存在節點B,接着通過對比節點位置判斷是否進行移動操作。B在舊集合中的位置B._mountIndex = 1,此時lastIndex = 0,不滿足child._mountIndex < lastIndex的條件,因此不對B進行移動。更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中prevChild._mountIndex表示B在舊集合中的位置,則lastIndex = 1。並將B的位置更新爲新集合中的位置prevChild._mountIndex = nextIdex,此時新集合中B._mountIndex = 0, nextIndex ++進入下一個節點判斷。
  • 從新集合中取得A, 然後判斷舊集合中是否存在A相同節點A,此時發現存在節點A。接着通過對比節點位置是否進行移動操作。A在舊集合中的位置A._mountIndex=0,此時lastIndex=1,滿足child._mountIndex < lastIndex條件,因此對A進行移動操作enqueueMove(this, child._mountIndex, toIndex),其中toIndex其實就是nextIndex,表示A需要移動的位置。更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),則lastIndex = 1。並將A的位置更新爲新集合中的位置prevChild._mountIndex = nextIndex,此時新集合中A._mountIndex = 1, nextIndex++ 進入下一個節點的判斷。
  • 從新集合中取得 D,然後判斷舊集合中是否存在相同節點 D,此時發現存在節點 D,接着通過對比節點位置判斷是否進行移動操作。D 在舊集合中的位置D._mountIndex = 3,此時 lastIndex = 1 ,不滿足 child._mountIndex < lastIndex 的條件,因此不對 D 進行移動操作。更新 lastIndex=Math.max(prevChild._mountIndex, lastIndex) ,則 lastIndex =

3 ,並將 D 的位置更新爲新集合中的位置 prevChild._mountIndex = nextIndex ,此時新集合中 D._mountIndex = 2 , nextIndex++ 進入下一個節點的判斷。

  • 從新集合中取得 C,然後判斷舊集合中是否存在相同節點 C,此時發現存在節點 C,接着

通過對比節點位置判斷是否進行移動操作。C 在舊集合中的位置 C._mountIndex = 2 ,此
時 lastIndex = 3 ,滿足 child._mountIndex < lastIndex 的條件,因此對 C 進行移動操作
enqueueMove(this, child._mountIndex, toIndex) 。更新 lastIndex = Math.max(prevChild.
_mountIndex, lastIndex) ,則 lastIndex = 3 ,並將 C 的位置更新爲新集合中的位置
prevChild._mountIndex = nextIndex ,此時新集合中 A._mountIndex = 3 , nextIndex++ 進
入下一個節點的判斷。由於 C 已經是最後一個節點,因此 diff 操作到此完成。

如果有新增的節點和刪除的節點diff如何處理呢?(以下都是複製的,碼不動字了....)

  • 從新集合中取得B,然後判斷舊集合中存在是否相同節點 B,可以發現存在節點 B。由於

B 在舊集合中的位置 B._mountIndex = 1 ,此時 lastIndex = 0 ,因此不對 B 進行移動操作。
更新 lastIndex = 1 ,並將 B 的位置更新爲新集合中的位置 B._mountIndex = 0 , nextIndex++
進入下一個節點的判斷。

  • 從新集合中取得 E,然後判斷舊集合中是否存在相同節點 E,可以發現不存在,此時可以

創建新節點 E。更新 lastIndex = 1 ,並將 E 的位置更新爲新集合中的位置, nextIndex++
進入下一個節點的判斷。

  • 從新集合中取得 C,然後判斷舊集合中是否存在相同節點 C,此時可以發現存在節點 C。

由於 C 在舊集合中的位置 C._mountIndex = 2 , lastIndex = 1 ,此時 C._mountIndex >
lastIndex ,因此不對 C 進行移動操作。更新 lastIndex = 2 ,並將 C 的位置更新爲新集
閤中的位置, nextIndex++ 進入下一個節點的判斷。

  • 從新集合中取得 A,然後判斷舊集合中是否存在相同節點 A,此時發現存在節點 A。由於

A 在舊集合中的位置 A._mountIndex = 0 , lastIndex = 2 ,此時 A._mountIndex < lastIndex ,
因此對 A 進行移動操作。更新 lastIndex = 2 ,並將 A 的位置更新爲新集合中的位置,
nextIndex++ 進入下一個節點的判斷。

  • 當完成新集合中所有節點的差異化對比後,還需要對舊集合進行循環遍歷,判斷是否存

在新集合中沒有但舊集合中仍存在的節點,此時發現存在這樣的節點 D,因此刪除節點 D,
到此 diff 操作全部完成。

這一篇讀的有點亂...稍微總結一下下:

  1. React 通過diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
  2. React 通過分層求異的策略,對 tree diff 進行算法優化;
  3. React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行算法優化;
  4. React 通過設置唯一 key的策略,對 element diff 進行算法優化;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章