開篇咱們先舉個簡單而且常見的例子:javascript
<ul id='list'>
<li class='item'>1</li>
<li class='item'>2</li>
<li class='item'>3</li>
</ul>
複製代碼
頁面上有個list,數據依次是一、二、3,如今須要替換成四、五、六、7,若是不使用React,應該怎麼操做?
方法1:removeChild()
清空列表,appendChild()
添加4個元素
方法2:針對前3個元素作nodeValue
/textContent
修改,而後appendChild()
添加1個元素
方法3:使用innerHtml
直接對整個列表作覆蓋式操做html
以上3種方式都是使用原生DOM API,均可以實現效果,可是性能上會存在差別。形成差別的緣由多種多樣,可能取決於元素類型,列表長度,甚至瀏覽器版本(萬惡的IE)。所以應當根據當前環境靈活選用不一樣的DOM操做方式,但這無疑增長了開發難度,不利於工程師專一實現當前業務。java
使用React的話就簡單多了,咱們無需關心如何進行DOM操做,只須要把數據存在state
中,而後在render
函數中map
生成JSX代碼。至於如何操做DOM,由React決定,這些都得益於虛擬DOM和diff算法。node
虛擬DOM就是使用javascript對象來表示真實DOM,是一個樹形結構。
在真實DOM中,一個普通的div打印出來也是很複雜的: git
const tree = {
tagName: 'ul', // 節點標籤名
props: { // DOM的屬性,用一個對象存儲鍵值對
id: 'list'
},
children: [ // 該節點的子節點
{tagName: 'li', props: {class: 'item'}, children: ['1']},
{tagName: 'li', props: {class: 'item'}, children: ['2']},
{tagName: 'li', props: {class: 'item'}, children: ['3']},
]
}
複製代碼
虛擬DOM只保留了真實DOM節點的一些基本屬性,和節點之間的層次關係,它至關於創建在javascript和DOM之間的一層「緩存」,能夠類比CPU和硬盤:硬盤的讀寫速度是很慢的,相比較於廉價的內存來講。github
React須要同時維護兩棵虛擬DOM樹:一棵表示當前的DOM結構,另外一棵在React狀態變動將要從新渲染時生成。React經過比較這兩棵樹的差別,決定是否須要修改DOM結構,以及如何修改。這種算法稱做Diff算法。算法
這個算法問題有一些通用的解決方案,即生成將一棵樹轉換成另外一棵樹的最小操做數。 然而,即便在最前沿的算法中,該算法的複雜程度爲
O(n 3 )
,其中n
是樹中元素的數量。
若是在 React 中使用了該算法,那麼展現 1000 個元素所須要執行的計算量將在十億的量級範圍。這個開銷實在是太太高昂。因而 React 在如下兩個假設的基礎之上提出了一套O(n)
的啓發式算法:數組
- 兩個不一樣類型的元素會產生出不一樣的樹;
- 開發者能夠經過
key prop
來暗示哪些子元素在不一樣的渲染下能保持穩定;
有人說虛擬DOM快,這也是相比較來講的。因爲維護虛擬DOM樹和Diff算法的計算,簡化了DOM操做,和MVVM變動整個DOM樹的模式相比,確實節省了很多時間。可是和原生JS的開發模式相比,仍是直接操做DOM比較快一些,由於開發者明確地知道應該變動哪部分的DOM結構(前提是開發者瞭解最基本的DOM優化方法)。瀏覽器
Diff算法會對新舊兩棵樹作深度優先遍歷,避免對兩棵樹作徹底比較,所以算法複雜度能夠達到O(n)
。而後給每一個節點生成一個惟一的標誌:緩存
在遍歷的過程當中,每遍歷到一個節點,就將新舊兩棵樹做比較,而且只對同一級別的元素進行比較:
也就是隻比較圖中用虛線鏈接起來的部分,把先後差別記錄下來。
可能存在的差別類型以下:
舉個例子,當一個元素從<a>
變成<img>
,從<Article>
變成<Comment>
,或從 <Button>
變成<div>
,都會觸發一個完整的重建流程:該節點以及該節點的子節點,都會被銷燬,以後建立新的節點。即 removeChild()
-> appendChild()
或者 setInnerHTML()
。
<div className="before" title="stuff" />
<div className="after" title="stuff" />
複製代碼
當比對兩個相同類型的元素時,React 會保留當前 DOM 節點和子節點,僅比對及更新有改變的屬性。 經過比對這兩個元素,React 知道只須要修改 DOM 元素上的 className
屬性。即 removeAttribute()
-> setAttribute()
或者 setAttribute()
。
另外,React會針對style的改變作特殊處理:
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
複製代碼
經過比對這兩個元素,React 知道只須要修改 DOM 元素上的 color
樣式,無需修改 fontWeight
。
若是是React組件,當一個組件更新時,組件實例保持不變,而且更新組件狀態,調用該實例的 componentWillReceiveProps()
和 componentWillUpdate()
方法。下一步,調用 render()
方法,diff 算法將在以前的結果以及新的結果中進行遞歸。
<div>text 1</div>
<div>text 2</div>
複製代碼
文本節點改動時,React會修改 nodeValue
/textContent
,這點無需贅述。
在默認條件下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表;當產生差別時,生成一個 mutation。
在子元素列表末尾新增元素時,更變開銷比較小。好比:
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
複製代碼
經過比較,前兩個元素不存在差別,只須要在末尾插入一個<li>third</li>
。
若是簡單實現的話,在列表頭部插入會很影響性能,好比:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
複製代碼
根據以前提到的算法規則,<li>Duke</li>
和<li>Villanova</li>
是不能被保留的,都會被看作差別部分,這樣的變動開銷會比較大。
爲了解決以上問題,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>
複製代碼
Diff算法經過比較得知,key
爲'2014'的元素是新增的,key
爲'2015'和'2016'的元素僅僅是移動了位置,因此能夠調用insertBefore()
來插入節點。
生成key的注意點:
key
在列表中應當具備惟一性,但不須要全局惟一。key
應當具備穩定性,一個節點在肯定key
以後就不該當變動key
(除非你但願它從新渲染)。不穩定的 key
(好比在 render()
中經過 Math.random()
生成的 )會致使許多組件實例和 DOM 節點被沒必要要地從新建立,這可能致使性能降低和子組件中的狀態丟失。key
。這個策略在元素不進行從新排序時比較合適,但一旦有順序修改,diff 就會變得慢。當基於下標的組件進行從新排序時,組件
state
可能會遇到一些問題。因爲組件實例是基於它們的key
來決定是否更新以及複用,若是key
是一個下標,那麼修改順序時會修改當前的key
,致使非受控組件的state
(好比輸入框)可能相互篡改致使沒法預期的變更。
key
的使用方式是關乎於性能的,應儘可能避免以數組下標做爲key
,而是以id的形式,或者數組下標 + id/name
。參考文章: