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