談談React虛擬DOM和diff算法

1、開篇

開篇咱們先舉個簡單而且常見的例子:javascript

<ul id='list'>
  <li class='item'>1</li>
  <li class='item'>2</li>
  <li class='item'>3</li>
</ul>
複製代碼

頁面上有個list,數據依次是一、二、3,如今須要替換成四、五、六、7,若是不使用React,應該怎麼操做?
方法1removeChild()清空列表,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

2、什麼是虛擬DOM?

虛擬DOM就是使用javascript對象來表示真實DOM,是一個樹形結構。
在真實DOM中,一個普通的div打印出來也是很複雜的: git

能夠想象瀏覽器處理DOM結構有多慢!和操做DOM相比,操做javascript對象更方便快速,開篇的例子中的DOM結構就能夠用javascript來表示:

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

3、什麼是Diff算法?

React須要同時維護兩棵虛擬DOM樹:一棵表示當前的DOM結構,另外一棵在React狀態變動將要從新渲染時生成。React經過比較這兩棵樹的差別,決定是否須要修改DOM結構,以及如何修改。這種算法稱做Diff算法。算法

這個算法問題有一些通用的解決方案,即生成將一棵樹轉換成另外一棵樹的最小操做數。 然而,即便在最前沿的算法中,該算法的複雜程度爲 O(n 3 ),其中 n 是樹中元素的數量。
若是在 React 中使用了該算法,那麼展現 1000 個元素所須要執行的計算量將在十億的量級範圍。這個開銷實在是太太高昂。因而 React 在如下兩個假設的基礎之上提出了一套 O(n) 的啓發式算法:數組

  1. 兩個不一樣類型的元素會產生出不一樣的樹;
  2. 開發者能夠經過 key prop 來暗示哪些子元素在不一樣的渲染下能保持穩定;

有人說虛擬DOM快,這也是相比較來講的。因爲維護虛擬DOM樹和Diff算法的計算,簡化了DOM操做,和MVVM變動整個DOM樹的模式相比,確實節省了很多時間。可是和原生JS的開發模式相比,仍是直接操做DOM比較快一些,由於開發者明確地知道應該變動哪部分的DOM結構(前提是開發者瞭解最基本的DOM優化方法)。瀏覽器

4、Diff算法的具體過程

Diff算法會對新舊兩棵樹作深度優先遍歷,避免對兩棵樹作徹底比較,所以算法複雜度能夠達到O(n)。而後給每一個節點生成一個惟一的標誌緩存

在遍歷的過程當中,每遍歷到一個節點,就將新舊兩棵樹做比較,而且只對同一級別的元素進行比較

也就是隻比較圖中用虛線鏈接起來的部分,把先後差別記錄下來。
可能存在的差別類型以下:

1. 不一樣類型的元素

舉個例子,當一個元素從<a>變成<img>,從<Article>變成<Comment>,或從 <Button>變成<div>,都會觸發一個完整的重建流程:該節點以及該節點的子節點,都會被銷燬,以後建立新的節點。即 removeChild() -> appendChild() 或者 setInnerHTML()

2. 同類型的元素

<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 算法將在以前的結果以及新的結果中進行遞歸。

3. 文本節點

<div>text 1</div>
<div>text 2</div>
複製代碼

文本節點改動時,React會修改 nodeValue/textContent,這點無需贅述。

4. 移動、刪除、新增子節點

在默認條件下,當遞歸 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>是不能被保留的,都會被看作差別部分,這樣的變動開銷會比較大。

5、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>
複製代碼

Diff算法經過比較得知,key爲'2014'的元素是新增的,key爲'2015'和'2016'的元素僅僅是移動了位置,因此能夠調用insertBefore()來插入節點。

生成key的注意點:

  • key在列表中應當具備惟一性,但不須要全局惟一。
  • key應當具備穩定性,一個節點在肯定key以後就不該當變動key(除非你但願它從新渲染)。不穩定的 key(好比在 render() 中經過 Math.random() 生成的 )會致使許多組件實例和 DOM 節點被沒必要要地從新建立,這可能致使性能降低和子組件中的狀態丟失。
  • 可使用元素在數組中的下標做爲 key。這個策略在元素不進行從新排序時比較合適,但一旦有順序修改,diff 就會變得慢。

當基於下標的組件進行從新排序時,組件 state 可能會遇到一些問題。因爲組件實例是基於它們的 key 來決定是否更新以及複用,若是 key 是一個下標,那麼修改順序時會修改當前的 key,致使非受控組件的 state(好比輸入框)可能相互篡改致使沒法預期的變更。

6、總結

  1. React和Vue都實現了虛擬DOM和diff算法,把操做DOM的部分封裝起來,讓開發者專一於業務實現,在保證開發效率的前提下,儘量的提高DOM操做性能。
  2. key 的使用方式是關乎於性能的,應儘可能避免以數組下標做爲key,而是以id的形式,或者數組下標 + id/name

參考文章:

深度剖析:如何實現一個 Virtual DOM 算法

協調-React.js

相關文章
相關標籤/搜索