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)算法:
- 不同類型的兩個元素將產生不同的樹。
- 開發者可以暗示,在具有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依賴于啟發式算法,如果不能滿足該算法設定的假設,性能將受到影響。
- 該算法不會嘗試匹配不同組件類型的子樹。如果你發現自己在兩個具有非常相似輸出的組件類型之間徘徊,你應該使它們類型相同。在實踐中,我們沒有發現這是一個問題。
- key應該是穩定、可預測和唯一的。不穩定的key(如由
Math.random()
產生的)將導致許多組件實例和DOM節點被不必要地重新創建,這可能導致子組件性能降級和丟失狀態。