淺談React中的diff

簡介

diff算法在React中處於主導地位,是React V-dom和渲染的性能保證,這也是React最有魅力、最吸引人的地方。
React一個很大一個的設計有點就是將diff和V-dom的完美結合,而高效的diff算法可讓用戶更加自由的刷新頁面,讓開發者也能遠離原生dom操做,更加開心的擼代碼。
但總所周知,普適diff的複雜度對於大量dom對比會出現嚴重的性能問題,React團隊對diff的優化可讓React可以在服務端渲染,到底React的diff作了什麼優化呢?本文來簡單探討一下!react

React的diff策略

  1. 策略一:忽略Web UI中DOM節點跨層級移動;
  2. 策略二:擁有相同類型的兩個組件產生的DOM結構也是類似的,不一樣類型的兩個組件產生的DOM結構則不近相同
  3. 策略三:對於同一層級的一組子節點,經過分配惟一惟一id進行區分(key值) 在Web UI的場景下,基於以上三個點,React對tree diff、component diff、element diff進行優化,將普適diff的複雜度下降到一個數量級,保證了總體UI界面的構建性能!

三個優化

tree diff算法

基於策略一,React的作法是把dom tree分層級,對於兩個dom tree只比較同一層次的節點,忽略Dom中節點跨層級移動操做,只對同一個父節點下的全部的子節點進行比較。若是對比發現該父節點不存在則直接刪除該節點下全部子節點,不會作進一步比較,這樣只須要對dom tree進行一次遍歷就完成了兩個tree的比較。
==那麼對於跨層級的dom操做是怎麼進行處理的呢?==下面經過一個圖例進行闡述app

兩個tree進行對比,右邊的新tree發現A節點已經沒有了,則會直接銷燬A以及下面的子節點B、C;在D節點上面發現多了一個A節點,則會從新建立一個新的A節點以及相應的子節點。
具體的操做順序:create A → create B → creact C → delete A。dom

優化建議
性能

保證穩定dom結構有利於提高性能,不建議頻繁真正的移除或者添加節點
複製代碼

component diff優化

React應用是基於組件構建的,對於組件的比較優化側重於如下幾點:
1. 同一類型組件聽從tree diff比較v-dom樹 2. 不通類型組件,先將該組件歸類爲dirty component,替換下整個組件下的全部子節點 3. 同一類型組件Virtual Dom沒有變化,React容許開發者使用shouldComponentUpdate()來判斷該組件是否進行diff,運用得當能夠節省diff計算時間,提高性能this

如上圖,當組件D → 組件G時,diff判斷爲不一樣類型的組件,雖然它們的結構類似甚至同樣,diff仍然不會比較兩者結構,會直接銷燬D及其子節點,而後新建一個G相關的子tree,這顯然會影響性能,官方雖然認定這種狀況極少出現,可是開發中的這種現象形成的影響是很是大的。spa

優化建議
設計

對於同一類型組件合理使用shouldComponentUpdate(),應該避免結構相同類型不一樣的組件
複製代碼

element diffcode

對於同一層級的element節點,diff提供瞭如下3種節點操做:
1. INSERT_MARKUP 插入節點:對全新節點執行節點插入操做 2. MOVE_EXISING 移動節點:組件新集合中有組件舊集合中的類型,且element可更新,即組件調用了receiveComponent,這時能夠複用以前的dom,執行dom移動操做 3. REMOVE_NODE 移除節點:此時有兩種狀況:組件新集合中有組件舊集合中的類型,但對應的element不可更新、舊組建不在新集合裏面,這兩種狀況須要執行節點刪除操做

key值diff中重要性

通常diff在比較集合[A,B,C,D]和[B,A,D,C]的時候會進行所有對比,即按對應位置逐個比較,發現每一個位置對應的元素都有所更新,則把舊集合所有移除,替換成新的集合,如上圖,可是這樣的操做在React中顯然是複雜、低效、影響性能的操做,由於新集合中全部的元素均可以進行復用,無需刪除從新建立,耗費性能和內存,只須要移動元素位置便可。 React對這一現象作出了一個高效的策略:容許開發者對同一層級的同組子節點添加惟一key值進行區分。意義就是代碼上的一小步,性能上的一大步,甚至是翻天覆地的變化!

==重點來了,React經過key是如何進行element管理的呢?爲什麼如此高效?==

算法改進:
React會先進行新集合遍歷,for(name in nextChildren),經過key值判斷兩個對比集合中是否存在相同的節點,即if(prevChild === nextChild),如何爲true則進行移動操做,在此以前,須要執行被移動節點在新舊(child._mountIndex)集合中的位置比較,if(child._mountIndex < lastIndex)爲true時進行移動,不然不執行該操做,這其實是一種順序優化,lastIndex是不斷更新的,表示訪問過的節點在集合中的最右的位置。若當前訪問節點在舊集合中的位置比lastIndex大,即靠右,說明它不會影響其餘元素的位置,所以不用添加到差別隊列中,不執行移動操做,反之則進行移動操做。

下圖示例:

  • nextIndex = 0,lastIndex = 0,重新集合中獲取B,在舊集合中發現相同節點B,舊集合中:B._mountIndex = 1,child._mountIndex < lastIndex ==> false,不執行移動操做,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex), prevChild._mountIndex === B._mountIndex ==> true,更新B在新集合中的位置:prevChild._mountIndex = nextIndex,在新集合中:B._mountIndex = 0,nextIndex++,進行下一個節點判斷。

  • nextIndex = 1,lastIndex = 1,重新集合中獲取A,在舊集合中發現相同節點A,舊集合中:A._mountIndex = 0,child._mountIndex < lastIndex ==> true,對A進行移動操做enqueueMove(this, child._mountIndex, toIndex),toIndex是A要被移動到的位置,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),更新A在新集合中的位置prevChild._mountIndex = nextIndex,在新集合中:A._mountIndex = 1,nextIndex++,進行下一個節點判斷。

  • nextIndex = 2,lastIndex = 1,重新集合中獲取D,在舊集合中發現相同節點D,舊集合中:D._mountIndex = 3,child._mountIndex < lastIndex ==> false,不執行移動操做,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex), prevChild._mountIndex === D._mountIndex ==> true,更新D在新集合中的位置:prevChild._mountIndex = nextIndex,在新集合中:D._mountIndex = 2,nextIndex++,進行下一個節點判斷。

  • nextIndex = 3,lastIndex = 3,重新集合中獲取C,在舊集合中發現相同節點C,舊集合中:C._mountIndex = 2,child._mountIndex < lastIndex ==> true,對C進行移動操做enqueueMove(this, child._mountIndex, toIndex),toIndex是C要被移動到的位置,更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),更新C在新集合中的位置prevChild._mountIndex = nextIndex,在新集合中:A._mountIndex = 3,nextIndex++,進行下一個節點判斷。

    • 因爲是最後一個節點,diff操做完成

==那麼,除了有可複用節點,新集合當有新插入節點,舊集合有須要刪除的節點呢?==
下圖示例:

對於這種狀況,React則是執行如下步驟:

  • nextIndex = 0,lastIndex = 0,重新集合中獲取B,在舊集合中發現相同節點B,舊集合中:B._mountIndex = 1,child._mountIndex < lastIndex ==> false,不執行移動操做,更新lastIndex = 1,更新B在新集合中的位置,nextIndex++,進行下一個節點判斷。
  • nextIndex = 1,lastIndex = 1,重新集合中獲取E,在舊集合中沒有發現相同節點E,nextIndex++進入下一個節點判斷。
  • nextIndex = 2,lastIndex = 1,重新集合中獲取C,在舊集合中發現相同節點C,舊集合中:C._mountIndex = 2,child._mountIndex < lastIndex ==> false,不對C進行移動操做,更新lastIndex = 2,更新C在新集合的位置,nextIndex++,進行下一個節點判斷。
  • nextIndex = 3,lastIndex = 2,重新集合中獲取A,在舊集合中發現相同節點A,舊集合中:A._mountIndex = 0,child._mountIndex < lastIndex ==> true,對A進行移動操做,更新lastIndex = 2,更新A在新集合中的位置,nextIndex++進入下一個節點判斷。
  • 當完成新集合全部節點中的差別對比後,對舊集合進行遍歷,判讀舊集合中是否存在新集合中不存在的節點,此時發現D節點符合判斷,執行刪除D節點的操做,diff操做完成。

優化後diff的不足

世上沒有百分之百完美算法,React的diff也有本身的不足之處,好比新舊集合元素所有能夠複用,只是新集合中將舊集合最後一個元素放到了第一個位置,短板就會出現 下圖示例:

按照上述順序優化,則舊集合中D的位置是最大的,最少的操做只是將D移動到第一位就能夠了,實際上diff操做會移動D以前的三個節點到對應的位置,這種狀況會影響渲染的性能。

優化建議

在開發過程當中,同層級的節點添加惟一key值能夠極大提高性能,儘可能減小將最後一個節點移動到列表首部的操做,當節點達到必定的數量之後或者操做過於頻繁,在必定程度上會影響React的渲染性能。好比大量節點拖拽排序的問題。
複製代碼

總之,React爲咱們提供優秀的diff算法,使咱們可以在實際開發中happy的擼代碼,但也不是說能夠「隨意」去構建咱們的應用,根據diff的特色,在具體場景中取長補短,規避一些算法上面的短板也是有利於提高應用總體的性能。

參考資料:

  • 《深刻React技術棧》陳屹 ——3.5章節
相關文章
相關標籤/搜索