React 中的 diff 演算法

zjy4fun發表於2024-10-07

React diff

  1. 為什麼使用虛擬 DOM ?
    瀏覽器在處理 DOM 的時候會很慢,處理 JavaScript 會很快,頁面複雜的時候,頻繁操作 DOM 會有很大的效能開銷(每次資料變化都會引起整個 DOM 樹的重繪和重排)。
    為了避免頻繁操作 DOM,React 會維護兩個虛擬 DOM,如果有資料更新,會藉此計算出所有修改的狀態集中到一起,統一更新一次虛擬 DOM。
  2. diff 是什麼?
    React 會維護兩個虛擬 DOM,如果有資料更新,會藉此計算出所有修改的狀態,然後將這些變化更新到真實 DOM 上,diff 演算法就是比較兩個虛擬 DOM 樹的策略。
  3. 虛擬 DOM 一定會提高效能嗎?
    不一定。
    因為虛擬 DOM 雖然會減少 DOM 操作,但也無法避免 DOM 操作。
    它的優勢是在於 diff 演算法和批次處理策略,將所有的 DOM 操作蒐集起來,一次性去改變真實的 DOM,但在首次渲染上,虛擬 DOM 會多了一層計算,消耗一些效能,所以有可能會比 html 渲染的要慢

傳統 diff 演算法

透過迴圈遞迴對節點進行依此對比,其演算法複雜度達到了O(n^ 3),也就是說,如果展示一千個節點,就要計算十億次。

React v16 最佳化

React透過三大策略完成了最佳化:

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

分別對應:tree diff、component diff、element diff

tree diff

同級比較:在對比的過程中,如果發現節點不在了,會完全刪除不會對其他地方進行比較,這樣只需要對樹遍歷一次就OK了

component diff

兩種策略:

  1. 相同型別的元件

按照層級比較繼續比較虛擬DOM樹即可。

特殊情況:當元件A如果變化為元件B的時候,有可能虛擬DOM並沒有任何變化,所以使用者可以透過shouldComponentUpdate() 來判斷是否需要更新,判斷是否計算

  1. 不同型別的元件

React會直接判定該元件為dirty component(髒元件),無論結構是否相似,只要判斷為髒元件就會直接替換整個元件的所有節點

element diff

節點比較,對於同一層級的一子自節點,透過唯一的key進行比較

當所有節點處以同一層級時,React 提供了三種節點操作:

  1. 插入(INSERT_MARKUP):新的 component 型別不在老集合裡, 即是全新的節點,需要對新節點執行插入操作。
  2. 移動(MOVE_EXISTING):在老集合有新 component 型別,且 element 是可更新的型別,generateComponentChildren 已呼叫 receiveComponent,這種情況下prevChild=nextChild,就需要做移動操作,可以複用以前的 DOM 節點。(傳統 diff 檢測到不相同就直接刪除重建)
  3. 刪除(REMOVE_NODE):老 component 型別,在新集合裡也有,但對應的 element 不同則不能直接複用和更新,需要執行刪除操作,或者老 component 不在新集合裡的,也需要執行刪除操作。

具體的 diff 過程
遍歷新的一層節點,使用 lastIndex 記錄每次比較後的節點最新索引,使用 index 表示在舊的一層中該節點的索引。

在新的一層中,如果發現該節點存在過,透過 key 找到該節點在舊的一層中的座標 index,如果 index < lastIndex 就移動該節點,否則不動,然後更新 lastIndex = max(lastIndex, index)

如果該節點沒有存在過,就新建,但是不更新 lastIndex

相同位置

插入新節點

當最後一個節點移動到第一個位置時,可能出現的問題(所有節點都需要移動)

為什麼不能使用 index 作為 key 值?
因為透過 key 找到的兩個節點不相同,需要刪除重建,使用唯一值(比如id)描述每個節點,這樣起到 diff 演算法的作用。
下面的例子中的節點全都需要重建,因為每次透過 key 找到的節點都不相同,無法發揮 diff 演算法的作用

參考

https://juejin.cn/post/7116326409961734152

相關文章