React 的 diffing 算法

參考資料

協調 - React 中文文檔

一、為什么需要 diffing 算法?

在某一時間節點調用 Reactrender() 方法,會創建一棵由 React 元素組成的樹。在下一次 stateprops 更新時,相同的 render() 方法會返回一棵不同的樹。React 需要基于這兩棵樹之間的差別來判斷如何有效率的更新 UI 以保證當前 UI 與最新的樹保持同步。

這個算法問題有一些通用的解決方案,即生成將一棵樹轉換成另一棵樹的最小操作數。 然而,即使在最前沿的算法中,該算法的復雜程度為 O(n<sup> 3 </sup>),其中 n 是樹中元素的數量。

如果在 React 中使用了該算法,那么展示 1000 個元素所需要執行的計算量將在 十億 的量級范圍。這個開銷實在是太過高昂。于是 React 在以下兩個假設的基礎之上提出了一套 O(n) 的啟發式算法:

二、diffing 算法的復雜程度為?

O(n)

三、能做到如此低的算法復雜程度的兩個假設基礎

  1. 兩個不同類型的元素會產生出不同的樹;
  2. 開發者可以通過 key、prop 來暗示哪些子元素在不同的渲染下能保持穩定;

在實踐中,我們發現以上假設在幾乎所有實用的場景下都成立。

四、diffing 算法具體對比

總結:

  1. 對比不同類型的普通元素:當根節點為不同類型的元素時,React 會拆卸原有的樹并且建立起新的樹。
  2. 對比同一類型的普通元素:① 元素的屬性改變時,React 會保留 DOM 節點,僅比對及更新有改變的屬性。② 特殊的 style 屬性改變時,React 僅更新有所更變的 style 里的屬性。
  3. 對比同一類型的組件元素:不改變組件的 state;更新組件的 props;調用實例的相關方法;遞歸新舊結果。
  4. 對子節點進行遞歸:① 在子元素列表末尾新增元素時,更新開銷比較小,只新增末尾元素。② 將新增元素插入到表頭,那么更新開銷會比較大,會重建每一個子元素。

1. 對比不同類型的元素

當根節點為不同類型的元素時,React 會拆卸原有的樹并且建立起新的樹。舉個例子,當一個元素從 <a> 變成 <img>,從 <Article> 變成 <Comment>,或從 <Button> 變成 <div> 都會觸發一個完整的重建流程。

當拆卸一棵樹時,對應的 DOM 節點也會被銷毀。組件實例將執行 componentWillUnmount() 方法。當建立一棵新的樹時,對應的 DOM 節點會被創建以及插入到 DOM 中。組件實例將執行 UNSAFE_componentWillMount() 方法,緊接著 componentDidMount() 方法。所有跟之前的樹所關聯的 state 也會被銷毀。

在根節點以下的組件也會被卸載,它們的狀態會被銷毀。比如,當比對以下更變時:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

React 會銷毀 Counter 組件并且重新裝載一個新的組件。

注意:

這些方法被認為是過時的,在新的代碼中應該避免使用它們

  • UNSAFE_componentWillMount()

2. 對比同一類型的元素

當對比兩個相同類型的 React 元素時,React 會保留 DOM 節點,僅比對及更新有改變的屬性。比如:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

過對比這兩個元素,React 知道只需要修改 DOM 元素上的 className 屬性。

當更新 style 屬性時,React 僅更新有所更變的屬性。比如:

<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />

通過對比這兩個元素,React 知道只需要修改 DOM 元素上的 color 樣式,無需修改 fontWeight。

在處理完當前節點之后,React 繼續對子節點進行遞歸。

3. 對比同類型的組件元素

當一個組件更新時,組件實例保持不變,這樣 state 在跨越不同的渲染時保持一致。React 將更新該組件實例的 props 以跟最新的元素保持一致,并且調用該實例的 UNSAFE_componentWillReceiveProps()、UNSAFE_componentWillUpdate() 以及 componentDidUpdate() 方法。

下一步,調用 render() 方法,diff 算法將在之前的結果以及新的結果中進行遞歸。

注意:

這些方法已過時,在新代碼中應避免使用它們

  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

流程大概如圖所示

4. 對子節點進行遞歸

在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表;當產生差異時,生成一個 mutation。

在子元素列表末尾新增元素時,更新開銷比較小。比如:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 會先匹配兩個 <li>first</li> 對應的樹,然后匹配第二個元素 <li>second</li> 對應的樹,最后插入第三個元素的 <li>third</li> 樹。

如果只是簡單的將新增元素插入到表頭,那么更新開銷會比較大。比如:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 不會意識到應該保留 <li>Duke</li><li>Villanova</li>,而是會重建每一個子元素 。這種情況會帶來性能問題。

五、diffing 算法對子節點進行遞歸的優化 —— Keys

為了解決對子元素進行遞歸時更新開銷會比較大的問題,React 支持 key 屬性。

當子元素擁有 key 時,React 使用 key 來匹配原有樹上的子元素以及最新樹上的子元素。以下例子在新增 key 之后使得之前的低效轉換變得高效:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

現在 React 知道只有帶著 '2014'key 元素是新元素,帶著 '2015' 以及 '2016'key 元素僅僅移動了。

現實場景中,產生一個 key 并不困難。你要展現的元素可能已經有了一個唯一 ID,于是 key 可以直接從你的數據中提?。?/p>

<li key={item.id}>{item.name}</li>

當以上情況不成立時,你可以新增一個 ID 字段到你的模型中,或者利用一部分內容作為哈希值來生成一個 key。這個 key 不需要全局唯一,但在列表中需要保持唯一。

最后,你也可以使用元素在數組中的下標作為 key。這個策略在元素不進行重新排序時比較合適,如果有順序修改,diff 就會變得慢。

當基于下標的組件進行重新排序時,組件 state 可能會遇到一些問題。由于組件實例是基于它們的 key 來決定是否更新以及復用,如果 key 是一個下標,那么修改順序時會修改當前的 key,導致非受控組件的 state(比如輸入框)可能相互篡改導致無法預期的變動。

在 Codepen 有兩個例子,分別為 展示使用下標作為 key 時導致的問題,以及不使用下標作為 key 的例子的版本,修復了重新排列,排序,以及在列表頭插入的問題 。

六、性能會有所損耗的假設

請謹記協調算法是一個實現細節。React 可以在每個 action 之后對整個應用進行重新渲染,得到的最終結果也會是一樣的。在此情境下,重新渲染表示在所有組件內調用 render 方法,這不代表 React 會卸載或裝載它們。React 只會基于以上提到的規則來決定如何進行差異的合并。

我們定期探索優化算法,讓常見用例更高效地執行。在當前的實現中,可以理解為一棵子樹能在其兄弟之間移動,但不能移動到其他位置。在這種情況下,算法會重新渲染整棵子樹。

由于 React 依賴探索的算法,因此當以下假設沒有得到滿足,性能會有所損耗:

  1. 該算法不會嘗試匹配不同組件類型的子樹。如果你發現你在兩種不同類型的組件中切換,但輸出非常相似的內容,建議把它們改成同一類型。在實踐中,我們沒有遇到這類問題。
  2. Key 應該具有穩定,可預測,以及列表內唯一的特質。不穩定的 key(比如通過 Math.random() 生成的)會導致許多組件實例和 DOM 節點被不必要地重新創建,這可能導致性能下降和子組件中的狀態丟失。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • > 本文重點:介紹React重構的起因和目的,理解Fiber tree單向鏈表結構中各屬性含義,梳理調度過程和核心...
    intopiece_檳閱讀 1,261評論 0 0
  • 寫這篇博客的初衷是為了加深自己對該知識點的理解,同時也是記錄一下自己的遇到的問題,避免自己以后再犯相同的錯誤。為了...
    魚玉玉玉玉閱讀 752評論 0 0
  • 背景 大家都在使用React,之前呢,也給大家分享過一次主題為“淺談Hooks&&生命周期”的內容。今天呢,作為延...
    賀賀v5閱讀 479評論 0 0
  • 推薦指數: 6.0 書籍主旨關鍵詞:特權、焦點、注意力、語言聯想、情景聯想 觀點: 1.統計學現在叫數據分析,社會...
    Jenaral閱讀 5,743評論 0 5
  • 城空了,有樹長出來 我的城死了 鑄起它的人,殺死它的人 不愿因為這件事而驕傲 一座城的終結 永遠因為終結這件事而顯...
    于十六閱讀 2,885評論 6 17