React Diff理解

Pivot發表於2019-03-04

前言

一提到React,學過的人都會想到提高效能的兩大神奇特色:虛擬DOM & diff演算法。React diff作為Virtual DOM的加速器,其演算法的改進優化是React整的介面渲染的基礎,以及效能提高的保障。雖然開發中不需要知道其執行機制,但是理解之後有助於更好的理解React元件的生命週期,以及優化React程式。

React diff表示什麼?表示React針對傳統的diff演算法進行了React風格的優化!

傳統diff演算法

計算一棵樹形結構轉換成另一棵樹形結構的最少操作,是一個複雜且值得研究的問題。

傳統 diff 演算法通過迴圈遞迴對節點進行依次對比,效率低下,演算法複雜度達到 O(n^3),其中 n 是樹中節點的總數。O(n^3) 到底有多可怕,這意味著如果要展示1000個節點,就要依次執行上十億次的比較。代價太高。

如果 React 只是單純的引入 diff 演算法而沒有任何的優化改進,那麼其效率是遠遠無法滿足前端渲染所要求的效能。

因此,想要將 diff 思想引入 Virtual DOM,就需要設計一種穩定高效的 diff 演算法,而 React 做到了!

那麼,React diff 到底是如何實現的呢?

React diff優化

傳統的diff演算法的複雜度為O(n^3),顯然無法滿足效能要求。Facebook工程師通過大膽的策略,將O(n^3)複雜度簡化成了O(n),怎麼做到的呢?

diff策略

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

基於以上三個前提策略,React團隊對傳統diff演算法優化基於三個策略(《深入React技術棧》等講的確實有點難理解且模糊,這邊經過理解給出了自己的理解)

  • a->tree diff
  • b->component diff
  • c->element diffd

優化策略a: tree diff

基於tree diff策略,React對Virtual DOM樹進行 分層比較、層級控制,只對相同顏色框內的節點進行比較(同一父節點的全部子節點),當發現某一子節點不在了直接刪除該節點以及其所有子節點,不會用於進一步的比較,在演算法層面上就是說只需要遍歷一次就可以了,而無需在進行不必要的比較,便能完成整個DOM樹的比較。

如圖:

React Diff理解

同屬於分層比較、層級控制範疇,還會出現DOM節點跨層級的移動操作(React中這種情況DOM節點不穩定,損害效能,所以開發中不推薦這種情況的出現),React diff怎麼解決的呢?如下圖情況:

React Diff理解

上面描述的是同一層次不同DOM節點範疇,React diff用趨近於‘暴力’的方式,並不是把A B C 直接拼接到 D 節點上,而是刪除A B C 三個節點之後在 D 下面在建立的 A B C。這裡不做詳細分析,想直觀理解該過程,建議閱讀這篇用在生命週期裡打log的方式展示上述過程

優化策略b: component diff

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

  • 對於同一型別的元件,根據Virtual DOM是否變化也分兩種,可以用shouldComponentUpdate()判斷Virtual DOM是否發生了變化,若沒有變化就不需要在進行diff,這樣可以節省大量時間,若變化了,就對相關節點進行update
  • 對於非同一類的元件,則將該元件判斷為 dirty component,從而替換整個元件下的所有子節點。

如下圖,當 component D 改變為 component G 時,即使這兩個 component 結構相似,一旦 React 判斷 D 和 G 是不同型別的元件,就不會比較二者的結構,而是直接刪除 component D,重新建立 component G 以及其子節點。雖然當兩個 component 是不同型別但結構相似時,React diff 會影響效能,但正如 React 官方部落格所言:不同型別的 component 是很少存在相似 DOM tree 的機會,因此這種極端因素很難在實現開發過程中造成重大影響的。

React Diff理解

而如果上圖中左一中的D節點只是單純的改變什麼state,update就好了。

優化策略c: element diff

所有同一層級的子節點.他們都可以通過key來區分-----並遵循策略a、b。

React Diff理解

沒經過優化的演算法,實現新老交替的方法是將A B C D全部刪除之後,在新建B A D C,這樣的實現方法顯然很垃圾,React diff怎麼優化呢?是通過為每一個節點新增key值標識。

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

上述分析的是新老集合中存在相同節點但是位置不同,要是有新加入的節點且有舊節點需要刪除呢?這裡不再囉嗦,如下圖:

React Diff理解

加了key的好處:

如果不加key,map遍歷的時候控制檯發出warn,既然是warn就說明不加也能實現遍歷,但是是經過刪除、建立、插入實現,這樣的話損害效能可想而知,而加上key就可以有助於React diff演算法結合Virtual DOM找到最合適的方式進行diff,最大限度的實現高效diff,即哪裡需要改變,就改變哪裡!

總結

  • React 通過分層求異的策略,對 tree diff 進行演算法優化;
  • React 通過相同類生成相似樹形結構,不同類生成不同樹形結構的策略,對 component diff 進行演算法優化;
  • React 通過設定唯一 key的策略,對 element diff 進行演算法優化;
  • React 通過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
  • 建議,開發時保持穩定的DOM結構有助於效能的提升;

參考

相關文章