搞懂React源碼系列-React Diff原理

時隔2年,從新看React源碼,不少之前不理解的內容如今都懂了。本文將用實際案例結合相關React源碼,集中討論React Diff原理。使用當前最新React版本:16.13.1html

另外,今年將寫一個「搞懂React源碼系列」,把React最核心內容用最通俗易懂地方式講清楚。2020年搞懂React源碼系列:react

  • React Diff原理git

  • React 調度原理github

  • 搭建閱讀React源碼環境-支持全部版本斷點調試算法

  • React Hooks原理數組

歡迎Star和訂閱個人博客微信

在討論Diff算法前,有必要先介紹React Fiber,由於React源碼中各類實現都是基於Fiber,包括Diff算法。固然,熟悉React Fiber的朋友可跳過Fiber介紹。架構

Fiber簡介

Fiber並不複雜,但若是要全面理解,仍是得花好一段時間。本文主題是diff原理,因此這裏僅簡單介紹下Fiber。ide

Fiber是一個抽象的節點對象,每一個對象可能有子Fiber(child)和相鄰Fiber(child)和父Fiber(return),React使用鏈表的形式將全部Fiber節點鏈接,造成鏈表樹。函數

Fiber還有反作用標籤(effectTag),好比替換Placement(替換)和Deletion(刪除),用於以後更新DOM。

值得注意的是,React diff中,除了fiber,還用到了基礎的React元素對象(如: 將<div>foo</div>編譯後生成的對象: { type: 'div', props: { children: 'foo' } } )。

Diff 過程

React源碼中,關於diff要從reconcileChildren(...)提及。

總流程:

流程圖中, 顯示源碼中用到的函數名,省略複雜參數。「新內容」即被比較的新內容,它多是三種類型:

  • 對象: React元素

  • 字符串或數字: 文本

  • 數組:數組元素多是React元素或文本

新內容爲React元素

咱們先以新內容爲React元素爲例,全面的調試一遍代碼,將以後會重複用到的方法在此步驟中講解,同時以一張流程圖做爲總結。

案例:

function SingleElementDifferentTypeChildA() { return <h1>A</h1> }

function SingleElementDifferentTypeChildB() { return <h2>B</h2> }

function SingleElementDifferentType() {

 const [ showingA, setShowingA ] = useState( true ) 

 useEffect( () => {

  setTimeout( () => setShowingA( false ), 1000 )

 } )

 return showingA ? <SingleElementDifferentTypeChildA/> : <SingleElementDifferentTypeChildB/>

}

ReactDOM.render( <SingleElementDifferentType/>, document.getElementById('container') )

複製代碼

從第一步reconcileChildren(...)開始調試代碼,無需關注與diff不相關的內容,好比renderExpirationTime。左側調試面板可看到對應變量的類型。

此處:

  • workInProgress: 父級Fiber

  • current.child: 處於比較中的舊內容對應fiber

  • nextChildren: 即處於比較中的新內容, 爲React元素,其類型爲對象。

在Diff時,比較中的舊內容爲Fiber,而比較中的新內容爲React元素、文本或數組。其實從這一步已經能夠看出,React官網的diff算法說明和實際代碼是實現差異較大。

由於新內容爲對象,因此繼續執行reconcileSingleElement(...)placeSingleChild(...)

咱們先看placeSingleChild(...)

placeSingleChild(...)的做用很簡單,給differ後的Fiber添加反作用標籤:Placement(替換),代表在以後須要將舊Fiber對應的DOM元素進行替換。

繼續看 reconcileSingleElement(...):

此處正式開始diff(比較),child爲舊內容fiber,element爲新內容,它們的元素類型不一樣。

由於類型不一樣,React將「刪除」舊內容fiber以及其全部相鄰Fiber(即給這些fiber添加反作用標籤 Deletion(刪除)), 並基於新內容生成新的Fiber。而後將新的Fiber設置爲父Fiber的child。

到此,一個新內容爲React元素的且新舊內容的元素類型不一樣的Diff過程已經完成。

那若是新舊內容的元素類型相同呢?

編寫相似案例,咱們能夠獲得結果

userFiber(...)

userFiber(...)的主要做用是基於舊內容fiber和新內容的屬性(props)克隆生成一個新內容fiber,這也是所謂的fiber複用。

因此當新舊內容的元素類容相同,React會複用舊內容fiber,結合新內容屬性,生成一個新的fiber。一樣,將新的fiber設置位父fiber的child。

新內容爲React元素的diff流程總結:

新內容爲文本

當新內容爲文本時,邏輯與新內容爲React元素時相似:

新內容爲數組

使用案例:

function ArrayComponent() {

  const [ showingA, setShowingA ] = useState( true ) 

  useEffect( () => {

   setTimeout( () => setShowingA( false ), 1000 )

  } )

  return showingA ? <div><span>A</span><span>B</span> </div> : <div><span>C</span> ​ D </div>

}

ReactDOM.render( <ArrayComponent/>, document.getElementById('container') )

複製代碼

若新內容爲數組,需reconcileChildrenArray(...):

for循環遍歷新內容數組,僞代碼(用於理解):

for ( let i = 0, oldFiber; i < newArray.length; ) {

  ...

  i++

  oldFiber = oldFiber.sibling
}
複製代碼

遍歷每一個新內容數組元素時:

updateSlot(...):

由於newChild的類型爲object, 因此:

updateElement(...):

updateElement(...)reconcileSingleElement(...)核心邏輯一致:

  • 若新舊內容元素類型一致,則克隆舊fiber,結合新內容生成新的fiber

  • 若不一致,則基於新內容建立新的fiber。

同理,updateTextNode(...)

updateTextNode(...)reconcileSingleTextNode(...)核心邏輯一致:

  • 若舊內容fiber的標籤不是HostText,則基於新內容文本建立新的fiber

  • 如果HostText, 則克隆舊fiber,結合新內容文本生成新的fiber

在本案例中,新內容數組for循環完成後:

由於新舊內容數組的長度一致,因此直接返回第一個新的fiber。而後同上,React將新的fiber設爲父fiber的child。

不過若新內容數組長度與舊內容fiber及其相鄰fiber的總個數不一致,React如何處理?

編寫相似案例。

若新內容數組長度更短:

React將刪除多餘的舊內容fiber的相鄰fiber。

若新內容數組長度更長:

React將遍歷多餘的新內容數組元素,基於新內容數組元素建立的新的fiber,並添加反作用標籤 Placement(替換)。

新內容爲數組時的diff流程總結:

總結

經過React源碼研究diff算法時,僅調試分析相關代碼,能比較容易的得出答案。

Diff的三種狀況:

  1. 新內容爲React元素
  2. 新內容爲文本
  3. 新內容爲數組

Diff時若比較結果相同,則複用舊內容Fiber,結合新內容生成新Fiber;若不一樣,僅經過新內容建立新fiber。

而後給舊內容fiber添加反作用替換標籤,或者給舊內容fiber及其全部相鄰元素添加反作用刪除標籤。

最後將新的(第一個)fiber設爲父fiber的child。

參考資料

感謝你花時間閱讀這篇文章。若是你喜歡這篇文章,歡迎點贊、收藏和分享,讓更多的人看到這篇文章,這也是對我最大的鼓勵和支持!

歡迎經過微信(掃描下方二維碼)或Github訂閱個人博客。

微信公衆號:蘇溪雲的博客
相關文章
相關標籤/搜索