React從入門到精通系列之(19)完全理解React如何從新處理DOM(Diffing算法)

十9、完全理解React如何從新處理DOM(Diffing算法)

React提供了一個聲明式的API,因此你沒必要擔憂每次DOM更新時內部會修改哪些東西。雖然在React中並非那麼明顯地告訴你具體如何實現的,不過這也讓編寫應用變得更加容易。javascript

本文會詳細解釋在React中的「diffing」算法是怎麼作的,以便組件更新是可預測的,從而讓高性能應用變得足夠快。java

動機

當使用React時,在單個時間點,您能夠將render()函數看作是在建立React元素樹。 在下一個stateprops更新時render()函數將返回一個不一樣的React元素樹。 React須要弄清楚如何高效地更新UI去匹配上最新的元素樹。算法

對於將一個樹變換成另外一個樹的最小操做數的算法問題,如今已經存在一些比較通用的解決方案。 然而,那些現有的最早進的技術算法都有O(n^3)的複雜度(n是樹中的元素的數量)。數組

若是在React中使用這些算法,顯示1000個元素將須要大約十億次比較。 這個真的代價太昂貴了。 相反,React實現了一個基於兩個假設直觀推斷出的O(n)算法:dom

  1. 不一樣類型的兩個元素將產生不一樣的樹。函數

  2. 開發人員能夠在不一樣渲染之間使用key屬性來表示哪些子元素是穩定的。性能

實際上,這兩條假設對幾乎全部的實際使用都是有效的。spa

Diffing算法

當比較兩棵DOM樹的差別時,React首先比較兩個根元素。 若是根元素的類型不一樣,那麼行爲也是不一樣的。code

不一樣類型的DOM元素

每當根元素是不一樣的類型時,React將刪除舊的DOM樹並從頭開始從新構建新的DOM樹。 從<a><img>、從<Article><Comment>、從<Button><div> ,只要不同就會徹底從新構建。component

當刪除就的DOM樹時,舊的DOM節點也被刪掉。 這個時候組件實例觸發componentWillUnmount()函數 。當構建一個新的DOM樹時,新的DOM節點會被插入到DOM中。 組件實例觸發componentWillMoun()componentDidMount()。 與以前舊的DOM樹相關聯的任何state也都將丟失。

在根元素之下的任何組件將被卸載而且它們的state也會所有丟失。 例如:

// 從
<div>
    <Counter />
</div>
// 變爲
<span>
    <Counter />
</span>

由於根元素從div變爲了span,因此舊的Counter組件將被銷燬,而後再從新構建一個新的。

相同類型的DOM元素

當比較相同類型的兩個React DOM元素時,React會先查看二者的屬性差別,而後保留相同的底層DOM節點,僅僅去更新那些被更改的屬性。 例如:

<div className="before" title="hello" />

<div className="after" title="hello" />

經過比較這兩個元素屬性,React就會知道只須要修改底層DOM節點上的className便可。

當更新style屬性時,React也會知道只須要更新style中的那些已更改的屬性。 例如:

<div style={{color: 'red', width: '300px'}} />

<div style={{color: 'red', width: '400px'}} />

當在這兩個元素之間轉換時,React知道只需修改width,而不是color

處理根DOM節點後,React會根據上面的判斷邏輯對子節點進行遞歸掃描。

相同類型的組件元素

當組件更新時,實例保持不變,所以在不一樣的渲染之間組件內的state是保持不變的。 React會更新底層組件實例的props來匹配新元素,並在底層實例上調用componentWillReceiveProps()componentWillUpdate()

接下來,調用render()方法,diff算法就會對上一個結果和新結果進行遞歸比較。

遞歸子元素

默認狀況下,當對DOM節點的子元素進行遞歸時,React只是同時迭代兩個子元素lists,並在有差別時產生變化。

例如,當在子元素的末尾再添加一個元素時,這兩個樹之間就會有一個的很好轉換效果:

<ul>
    <l1>one</li>
    <li>two</li>
</ul>

<ul>
    <li>one</li>
    <li>two</li>
    <li>three</li>
</ul>

React將匹配兩個<li>one</li>樹,匹配兩個<li>two</li>樹,而後插入一個<li>three</li>樹。

可是,不要太天真了。若是在子元素的開頭部分插入一個元素的話,性能會便的不好。 例如,這兩棵樹之間的轉換效果就不好:

<ul>
    <li>one</li>
    <li>two</li>
<ul>

<ul>
    <li>zero</li>
    <li>one</li>
    <li>two</li>
<ul>

這種狀況React將更改每一個子元素 ,而不會意識到它能夠保持<li>one</li><li>two</li>子元素樹無缺無損。 這種低效率的狀況是一個必須注意的問題。

keys

爲了解決上面的問題,React提供了一個key屬性。 當子元素有key屬性時,React使用key將原始樹中的子元素與後續樹中的子元素進行匹配。 例如,上面的那個低效例子添加一個key就可讓子元素樹轉換變的頗有效:

<ul>
    <li key="1">one</li>
    <li key="2">two</li>
<ul>

<ul>
    <li key="0">zero</li>
    <li key="1">one</li>
    <li key="2">two</li>
<ul>

如今React就能夠知道key="0"的元素是新的,而且key="1"key="2"的元素只需移動便可。

在實踐中,使用一個惟一的key並不難。 您要顯示的元素可能已具備惟一的ID,所以key能夠來自你本身的數據中:

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

若是不是這樣,你能夠向數據模型中給每一項數據添加一個新的ID屬性,或者對內容的某些部分進行哈希生成keykey屬性只有在其兄弟元素之間是惟一的,並非全局惟一的。

最後一種方式是能夠將數組中的索引做爲key。 若是數組中的每一項不須要從新排序,一樣也能夠很好地工做,可是萬一須要從新排序的話,這會變的很慢。

權衡利弊

要記住重要的是,diffing算法是一個具體的實現細節。 React能夠在每一個操做上去從新渲染應用; 最終結果都是同樣的。

在當前的實現中,你能夠看到一個事實是一個子樹已經成功移動到它的兄弟元素當中,但你不能告訴它已經移動到別的地方。 該算法將從新渲染這個完整的子元素樹。

由於React很依賴這個直觀推斷的算法來判斷DOM是否須要從新處理,若是不能知足這個算法的那兩個假設條件前提,應用的性能將會受到很大影響。

  1. 該算法不會去嘗試匹配那些不一樣組件類型的子元素樹。 若是你看到本身在返回類似輸出結果的兩個組件類型之間來來回回,你可能須要把它們改成相同的類型組件。

  2. key屬性應該是穩定,可預測和惟一的。 不穩定的鍵(如使用Math.random()生的key)將致使許多組件實例和DOM節點進行沒必要要地重複建立,這可能致使子組件中的性能下降和state丟失。

相關文章
相關標籤/搜索