8.Reconciliation

React版本:15.4.2
**翻譯:xiyoki **

React提供了一個聲明式API,所以你不必擔心每次更新的確切更改。這使得編寫應用程序更容易,但是在React中更新是如何實現的可能不明顯。本文解釋了我們在React的‘diffing’算法中做出的選擇,以便組件更新是可預測,同時高性能應用程序也足夠快。

Motivation

當你使用React時,在單個時間點,你可以將該render()函數想象為創建一個React元素樹。在下一個state或props更新時,該render()函數將返回一個不同的React元素樹。React然后需要找出如何有效地更新UI以匹配最新的樹。
對于生成將一個樹變換成另一個樹的最小操作數的算法問題,存在一些通用解決方案。然而, state of the art algorithms具有大約 O(n3)的復雜性,其中n是樹中的元素數量。
如果我們在React中使用它,顯示1000個元素將需要大約十億此比較。這太貴了。相反,React基于兩個假設實現啟發式O(n)算法:

  1. 不同類型的兩個元素將產生不同的樹。
  2. 開發者可以暗示,在具有key prop的不同渲染之間,哪些子元素是穩定的。

在實踐中,這些假設對于幾乎所有實際使用情況都是有效的。

The Diffing Algorithm(差分算法)

當差分兩棵樹時,React首先比較兩個根元素。根據根元素的類型,行為是不同的。

Elements Of Different Type(不同類型的元素)

每當根元素具有不同類型時,React將拆除舊樹并從頭開始構建新樹。從<a><img>, 或從<Article><Comment>, 或從<Button><div> - 任何這些將導致完全重建。
當拆除樹時,舊的DOM節點被銷毀。組件實例接收componentWillUnmount()。當構建新樹時,將新的DOM節點插入到DOM中。組件實例接收componentWillMount(),然后componentDidMount()。與舊樹相關聯的任何狀態都將丟失。
根下面的任何組件也將被卸載并且它們的狀態被銷毀。例如,當差分時:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

這將破壞舊的Counter,并重新安裝一個新的。

DOM Elements Of The Same Type(相同類型的DOM元素)

當比較相同類型的兩個React DOM元素時,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知道只修改color樣式,而不是修改fontWeight樣式。
處理DOM節點后,React然后對子節點進行遞歸。

Component Elements Of The Same Type(相同類型的組件元素)

當組件更新時,實例保持不變,so that state is maintained across renders。React更新底層組件實例的props以匹配新元素,并且在底層實例上調用 componentWillReceiveProps()componentWillUpdate()
接下來,render()方法被調用,diff算法對前一個結果和新結果進行遞歸。

Recursing On Children(在子元素上遞歸)

默認情況下,當對DOM節點的子節點進行遞歸時,React只是同時迭代這兩個字節點列表,并在有差異時生成一個變量。
例如,當在子元素的末尾添加一個元素時,這兩個數之間的轉換效果很好:

<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將改變每個child,而不是意識到它課可以保持<li>Duke</li><li>Villanova</li>子樹完好無損。這種低效率會是一個問題。

Keys

為了解決這個問題,React支持一個key屬性。當child有key屬性時,React使用key將原始樹中的child與后續樹中的child進行匹配。例如,添加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知道key為‘2014’的元素是新的,而key為'2015'和‘2016’的元素只是移動了一下。
在實踐中,尋找key通常不難。你要顯示的元素可能已具有唯一的ID,因此key可以來自你的數據:

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

如果不是這樣,你可以向模型中添加一個新的ID屬性,或hash內容的某些部分以生成key。
key只需在其兄弟之間是唯一的,而不是全局唯一的。
作為最后一種手段,你可以將數組中的項目索引作為key。這可以很好地工作,如果項目從來沒有重新排序,但重新排序會很慢。

Tradeoffs(權衡)

重點記住,reconciliation算法是一個實現細節。React可以在每個action上重新渲染整個應用程序;最終結果將是相同的。我們經常細化啟發式算法,以便使常見用例更快。
在當前的實現中,你可以表達這個事實,一個子樹已經被移動到它的兄弟姐妹中,但你不知道它已經被移動到別的地方。該算法將重新渲染該完整子樹。
因為React依賴于啟發式算法,如果不能滿足該算法設定的假設,性能將受到影響。

  1. 該算法不會嘗試匹配不同組件類型的子樹。如果你發現自己在兩個具有非常相似輸出的組件類型之間徘徊,你應該使它們類型相同。在實踐中,我們沒有發現這是一個問題。
  2. key應該是穩定、可預測和唯一的。不穩定的key(如由Math.random()產生的)將導致許多組件實例和DOM節點被不必要地重新創建,這可能導致子組件性能降級和丟失狀態。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 參考文章:深度剖析:如何實現一個Virtual DOM 算法 作者:戴嘉華React中一個沒人能解釋清楚的問題——...
    waka閱讀 5,994評論 0 21
  • 原教程內容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,860評論 1 18
  • 深入JSX date:20170412筆記原文其實JSX是React.createElement(componen...
    gaoer1938閱讀 8,106評論 2 35
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,381評論 25 708
  • 《流年》 我緊握手中 牽著流年的線 輕輕的 輕輕的 一拉 彈出了一些笑臉 喚起了一份記憶 然后 我在 輕輕的 輕輕...
    舊夢未歸閱讀 161評論 0 0