【React進階系列】 虛擬dom與diff算法

虛擬dom

Jsx 表面寫的是html,其實內部執行的是一段js
createElementhtml

React.createElement(
  type,
  [props],
  [...children]
)

createElement把這個樹形結構,存在內存裏面
Jsx最終以這樣的一個個對象遞歸的存在內存中,執行diff算法
clipboard.png
多層結構
clipboard.pngreact

簡單的createElement實現算法

clipboard.png
reactElement - 生成的是一個對象來描述這個節點dom

clipboard.png

react diff

與傳統樹的diff的區別性能

計算一棵樹形結構轉換成另外一棵樹形結構的最少操做,是一個複雜且值得研究的問題。傳統 diff 算法經過循環遞歸對節點進行依次對比,效率低下,算法複雜度達到 O(n^3)優化

react diff策略spa

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

tree diffcode

基於策略一,對樹進行分層比較,兩棵樹只會對同一層次的節點進行比較。  
 React 經過 updateDepth 對 Virtual DOM 樹進行層級控制,同一個父節點下的全部子節點。

clipboard.png

什麼是 DOM 節點跨層級的移動操做?component

A 節點(包括其子節點)整個被移動到 D 節點下

clipboard.png

若是出現了 DOM 節點跨層級的移動操做,React diff 會有怎樣的表現呢?htm

React 只會簡單的考慮同層級節點的位置變換,而對於不一樣層級的節點,只有建立和刪除操做。

當根節點發現子節點中 A 消失了,就會直接銷燬 A;當 D 發現多了一個子節點 A,則會建立新的 A(包括子節點)做爲其子節點。此時,React diff 的執行狀況:create A -> create B -> create C -> delete A。

注意:

在開發組件時,保持穩定的 DOM 結構會有助於性能的提高。例如,能夠經過 CSS 隱藏或顯示節點,而不是真的移除或添加 DOM 節點。

component diff

依據策略二

  • 若是是同一類型的組件,按照原策略繼續比較 virtual DOM tree。
  • 若是不是,則將該組件判斷爲 dirty component,從而替換整個組件下的全部子節點。
  • 對於同一類型的組件,有可能其 Virtual DOM 沒有任何變化,若是可以確切的知道這點那能夠節省大量的 diff 運算時間,所以 React 容許用戶經過 shouldComponentUpdate() 來判斷該組件是否須要進行 diff。

React 判斷 D 和 G 是不一樣類型的組件,就不會比較兩者的結構,而是直接刪除 component D,從新建立 component G 以及其子節點,即便D 和 G的結構很類似

clipboard.png

element diff

當節點處於同一層級時,React diff 提供了三種節點操做,分別爲:INSERT_MARKUP(插入)、MOVE_EXISTING(移動)和 REMOVE_NODE(刪除)。

  • INSERT_MARKUP,新的 component 類型不在老集合裏, 便是全新的節點,須要對新節點執行插入操做。
  • MOVE_EXISTING,在老集合有新 component 類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent,這種狀況下 prevChild=nextChild,就須要作移動操做,能夠複用之前的 DOM 節點。
  • REMOVE_NODE,老 component 類型,在新集合裏也有,但對應的 element 不一樣則不能直接複用和更新,須要執行刪除操做,或者老 component 不在新集合裏的,也須要執行刪除操做。

eg: 新老集合進行 diff 差別化對比,發現 B != A,則建立並插入 B 至新集合,刪除老集合 A;以此類推,建立並插入 A、D 和 C,刪除 B、C 和 D。

clipboard.png

帶來的問題:都是相同的節點,但因爲位置發生變化,致使須要進行繁雜低效的刪除、建立操做,其實只要對這些節點進行位置移動便可

react優化策略:容許開發者對同一層級的同組子節點,添加惟一 key 進行區分

clipboard.png

優化後diff實現:

  1. 對新集合的節點進行循環遍歷,經過惟一 key 能夠判斷新老集合中是否存在相同的節點
  2. 若是存在相同節點,則進行移動操做,但在移動前須要將當前節點在老集合中的位置child._mountIndex與lastIndex(訪問過的節點在老集合中最右的位置即最大的位置)進行比較,if (child._mountIndex < lastIndex),則進行節點移動操做

分析:

element  _mountIndex  lastIndex  nextIndex  enqueueMove
B        1            0          0          false
A        0            1          1          true
D        3            1          2          false
C        2            3          3          true

step:

重新集合中取得 B,判斷老集合中存在相同節點 B
B 在老集合中的位置 B._mountIndex = 1
初始 lastIndex = 0
不知足 child._mountIndex < lastIndex 的條件,所以不對 B 進行移動操做
更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex) lastIndex更新爲1
將 B 的位置更新爲新集合中的位置prevChild._mountIndex = nextIndex,此時新集合中 B._mountIndex = 0,nextIndex++

以上主要分析新老集合中存在相同節點但位置不一樣時,對節點進行位置移動的狀況,若是新集合中有新加入的節點且老集合存在須要刪除的節點,那麼 React diff 又是如何對比運做的呢?

clipboard.png

element  _mountIndex  lastIndex  nextIndex  enqueueMove
    B        1            0          0          false
    E        no exist
    C        2            1          2          false
    A        0            2          3          true

step

新建:重新集合中取得 E,判斷老集合中不存在相同節點 E,則建立新節點 E
     lastIndex不作處理
     E 的位置更新爲新集合中的位置,nextIndex++
刪除:當完成新集合中全部節點 diff 時,最後還須要對老集合進行循環遍歷,判斷是否存在新集合中沒有但老集合中仍存在的節點,發現存在這樣的節點 D,所以刪除節點 D

react diff的問題

理論上 diff 應該只需對 D 執行移動操做,然而因爲 D 在老集合的位置是最大的,致使其餘節點的 _mountIndex < lastIndex,形成 D 沒有執行移動操做,而是 A、B、C 所有移動到 D 節點後面的現象

clipboard.png

建議:在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。

總結:

  • React 經過制定大膽的 diff 策略,將 O(n3) 複雜度的問題轉換成 O(n) 複雜度的問題;
  • React 經過分層求異的策略,對 tree diff 進行算法優化;
  • React 經過相同類生成類似樹形結構,不一樣類生成不一樣樹形結構的策略,對 component diff 進行算法優化;
  • React 經過設置惟一 key的策略,對 element diff 進行算法優化;
  • 建議,在開發組件時,保持穩定的 DOM 結構會有助於性能的提高;
  • 建議,在開發過程當中,儘可能減小相似將最後一個節點移動到列表首部的操做,當節點數量過大或更新操做過於頻繁時,在必定程度上會影響 React 的渲染性能。
https://zhuanlan.zhihu.com/p/...
相關文章
相關標籤/搜索