協調(Reconciliation)
React提供了一組聲明式的API,讓你不必關心每次更新的變化,這樣使得應用的編寫容易了很多。
但是在React中如何實現還并不太清晰,這篇文章解釋了React對比算法的選擇,讓組件更新可預測并且使得高性能足夠的快。
目的
當你使用React,在單一的時間點,你可以考慮render()
函數作為創建React函數的樹,React需要算出如何更新UI來匹配最新的樹(dom)
有一個解決方案是:
將一棵樹轉換為另一棵樹的最小操作數算法問題的通用方案。然而樹種元素的個數為n,最先進的算法 的時間復雜度為O(n3) 。
如果我們在React中使用,展示1000個元素,則需要10億次的比較,這樣的操作臺昂貴。相反,React基于這兩點的假設,實現了一個啟發的O(n)算法:
①兩個不同類型的元素將產生兩顆不同的樹
②通過渲染器附帶的key
屬性,開發者可以示意,那些子元素是穩定的。
實踐中,這種假設適用于大部分的應用場景的。
對比算法
當對比兩棵樹時,React首先比較他們的根節點。根節點的type不同,他們的行為也不同。
不同類型的元素
每當根元素有不同的類型,React就會卸載舊樹,創建新樹,從<a>
到<img>
或從<Article>
到<Comment>
,或從<Button>
到 <div>
,任何的調整都會導致全部重建。
當樹被卸載,舊的DOM節點將被銷毀。組件實例會調用componentWillUnmount()
。當構建一棵新樹,新的DOM節點被插入到DOM中。組件實例將依次調用componentWillMount()
和componentDidMount()
。任何與舊樹有關的狀態都將丟棄。
這個根節點下,所有的組件都會被卸載,同時他們的狀態會被銷毀。
以下的節點對比前后:
<div>
<Counter />
</div>
<span>
<Counter />
</span>
由于根節點換了,所以組件<Counter>
將會重載新的組件。
相同類型的DOM元素
當比較2個相同的React DOM元素時,React則會觀察兩者的屬性。
當比較兩個相同類型的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遞歸其子元素。
相同類型的組件元素
當組建更新時,實例還是保持一致。這樣能讓狀態在渲染之間保留。React通過更新底層組件的props來渲染新的元素,并且在底層的組件上,依次調用componentWillReviceProps
和componentWillUpdate
的方法。
接下來render()
方法被調用,同時對比算法 遞歸處理之前的結果和新的結果。
遞歸子節點
默認情況下,當遞歸DOM節點的子節點,React只在同一個時間點,遞歸2個子節點列表。并且在有發生不同的時候,產生一個變更。
如,當在子節點末尾增加一個元素,兩棵樹的轉換效果很好:
<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會調整每一個子節點,而非意識到可以完整保留<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的元素。
實踐中,發現key通常不難。你將展示的元素可能已經帶有一個唯一的ID,因此key可以來自于你的數據中:
<li key={item.id}>{item.name}</li>
當這已不再是問題,你可以給你的數據增加一個新的ID屬性,或根據數據的某些內容創建一個哈希值來作為key。
key必須在其兄弟節點中是唯一的,而非全局唯一。
萬不得已,你可以傳遞他們在數組中的索引作為key。若元素沒有重排,該方法效果不錯,但重排會使得其變慢。
索引用作key時,組件狀態在重新排序時也會有問題。組件實例基于key進行更新和重用。如果key是索引,則item的順序變化會改變key值。這將導致受控組件的狀態可能會以意想不到的方式混淆和更新。
這里是在CodePen上使用索引作為鍵可能導致的問題的一個例子,這里是同一個例子的更新版本,展示了如何不使用索引作為鍵將解決這些reordering, sorting, 和 prepending的問題。
權衡
牢記協調算法的實現細節非常重要。React可能會在每次操作時渲染整個應用;而結果仍是相同的。為保證大多數場景效率能更快,我們通常提煉啟發式的算法。
在目前實現中,可以表明一個事實,即子樹在其兄弟節點中移動,但你無法告知其移動到哪。該算法會重渲整個子樹。
由于React依賴于該啟發式算法,若其背后的假設沒得到滿足,則其性能將會受到影響:
1.算法無法嘗試匹配不同組件類型的子元素。若你發現兩個輸出非常相似的組件類型交替出現,你可能希望使其成為相同類型。實踐中,我們并非發現這是一個問題。
2.Keys應該是穩定的,可預測的,且唯一的。不穩定的key(類似由Math.random()生成的)將使得大量組件實例和DOM節點進行不必要的重建,使得性能下降并丟失子組件的狀態。