React進階筆記8(協調Reconciliation)

協調(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來渲染新的元素,并且在底層的組件上,依次調用componentWillRevicePropscomponentWillUpdate的方法。

接下來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節點進行不必要的重建,使得性能下降并丟失子組件的狀態。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容