React 中 Virtual DOM 與 Diffing 算法的關係

前言

這篇文章是基於 React 官方文檔對於 Virtual DOM 的理念和 Diffing 算法的策略的整合。html

Virtual DOM 是一種編程理念

Virtual DOM 是一種編程理念。UI 信息被特定語言描述並保存到內存中,再經過特定的庫,例如 ReactDOM 與真實的 DOM 同步信息。這一過程成爲 協調 (Reconciliation)node

與之對應的數據結構

Virtual DOM 反映到實際的數據結構上,就是每個 React 的 fiber nodereact

// UI 組件描述
const Span = (props) => <span></span>

// 實際的 Fiber node structure
{
  stateNode: new HTMLSpanElement,
  type: "span",
  alternate: null,
  key: null,
  updateQueue: null,
  memoizedState: null,
  pendingProps: {},
  memoizedProps: {},
  tag: 1,
  effectTag: 0,
  nextEffect: null
}

這一抽離結構有點像 React 版本的 AST 抽象語法樹。算法

Diffing 算法

問題

在 Virtual DOM -> Real DOM 之間的轉換過程當中,須要高效率的算法來支撐。因爲某個時刻調用 React render() 方法生成的 React 元素組成的樹,與下一次 state 或 props 變化時調用同一個 render 返回的樹是不同的,React 須要根據這兩個不一樣的樹來決定如何高效地讓最新的 Virtual DOM 反應到真實 DOM 中。編程

解決方式

Diffing 算法就是解決如何更有效率地更新 UI 的關鍵。數組

React 採起了一個複雜度爲 O(n) 的比較策略,這個策略有兩個假設數據結構

  1. 兩個不一樣類型的元素會產出不一樣的樹
  2. 開發者能夠經過 key prop 來保持元素的穩定

Diffing 策略

1. 對比根節點的元素

若是爲不一樣類型,React 將會把原有的樹拆卸並從新創建新的樹。例如 <div> -> <span>ide

  1. 當這顆樹被拆卸後,對應的 DOM 節點也被銷燬,組件實例回調用 willUnmount 方法。
  2. 當創建新的樹的時候,對應的 DOM 將被插入到 DOM 中,並調用 didMount 方法。

在根節點如下的組件也會被卸載,它們的狀態會被銷燬。例如:函數

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

2. 對比同一類型的元素

當對比兩個同類型的 React 元素時,React 會保留 DOM 節點,僅對比以及更新有變化的屬性性能

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

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

經過對比兩個元素,React 得知 className 變化,因此只須要更新 DOM 對應元素上的 class

當處理完當前節點時,React 將會對子節點進行遞歸。

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

當一個 React 組件須要更新時(例如 props 有變化),組件實例保持不變,實例中的 state 能在不一樣渲染時保持一致。React 將更新該組件實例的 props 以保持與最新的元素的一致。並調用 該實例的原型 上的函數 getDerivedStateFromProps(官方文檔是 componentWillReceiveProps 和 componentWillUpdate,但這將會被棄用)。

下一步是調用該實例的 render 方法,diffing 算法將在以前的結果和最新的結果中進行遞歸。

4. 對子節點進行遞歸

問題

在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表,當發現兩個子元素有差別時,將生成一個「變種(mutation)」。

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

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

// after
<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 會認爲每一個子元素都「改變(mutate)」了,而不會認爲能夠保持 <li>Duke</li><li>Villanova</li> 子樹不變,從而致使從新渲染。這種狀況下的低效可能會帶來性能問題。

解決策略 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 的元素僅僅移動了位置。

因此通常在開發的時候最好使用一個有惟一屬性的 id 來做爲 key

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

在開發者本身肯定數組數據不會輕易改變的狀況下才能夠用數組下表來做爲 key。

權衡(Tradeoffs)

上述只是 協調算法(reconciliation algorithm)的實現細節而已。React 能夠響應每一次 action 後從新渲染整個應用,最終結果也會是同樣的。

須要明確知道的是,在當前上下文(this context)從新渲染(rerender)意味着會調用全部的 componentrender(),但並不意味着 React 會卸載(unmount)重載(remount)它們。它(協調算法)只會用上述規則在其過程當中找出不一樣。

參考

相關文章
相關標籤/搜索