深入淺出畫圖講解React Diff原理【實踐】


深入淺出畫圖講解React Diff原理【實踐】

轉發鏈接:https://segmentfault.com/a/1190000022311760

時隔2年,重新看React源碼,很多以前不理解的內容現在都懂了。本文將用實際案例結合相關React源碼,集中討論React Diff原理。使用當前最新React版本:16.13.1。

深入淺出畫圖講解React Diff原理【實踐】

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

React Diff原理React 調度原理搭建閱讀React源碼環境-支持所有版本斷點調試React Hooks原理

歡迎Star和訂閱我的博客。

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

Fiber簡介

Fiber並不複雜,但如果要全面理解,還是得花好一段時間。本文主題是diff原理,所以這裡僅簡單介紹下Fiber。

深入淺出畫圖講解React Diff原理【實踐】

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

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

值得注意的是,React diff中,除了fiber,還用到了基礎的React元素對象(如: 將

foo
編譯後生成的對象: { type: 'div', props: { children: 'foo' } } )。

Diff 過程

React源碼中,關於diff要從reconcileChildren(...)說起。

總流程:


深入淺出畫圖講解React Diff原理【實踐】

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

  • 對象: React元素
  • 字符串或數字: 文本
  • 數組:數組元素可能是React元素或文本

新內容為React元素

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

案例:

<code>
function SingleElementDifferentTypeChildA() { return

A

}

function SingleElementDifferentTypeChildB() { return

B

}

function SingleElementDifferentType() {

const [ showingA, setShowingA ] = useState( true )

useEffect( () => {

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

} )

return showingA ? <singleelementdifferenttypechilda> : <singleelementdifferenttypechildb>

}

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

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

深入淺出畫圖講解React Diff原理【實踐】

此處:

  • workInProgress: 父級Fiber
  • current.child: 處於比較中的舊內容對應fiber
  • nextChildren: 即處於比較中的新內容, 為React元素,其類型為對象。

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

深入淺出畫圖講解React Diff原理【實踐】

因為新內容為對象,所以繼續執行reconcileSingleElement(...)和placeSingleChild(...)。

我們先看placeSingleChild(...):

深入淺出畫圖講解React Diff原理【實踐】

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

繼續看 reconcileSingleElement(...):

深入淺出畫圖講解React Diff原理【實踐】

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

深入淺出畫圖講解React Diff原理【實踐】

深入淺出畫圖講解React Diff原理【實踐】

因為類型不同,React將“刪除”舊內容fiber以及其所有相鄰Fiber(即給這些fiber添加副作用標籤 Deletion(刪除)), 並基於新內容生成新的Fiber。然後將新的Fiber設置為父Fiber的child。

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

那如果新舊內容的元素類型相同呢?

編寫類似案例,我們可以得到結果

深入淺出畫圖講解React Diff原理【實踐】

userFiber(...):

深入淺出畫圖講解React Diff原理【實踐】

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

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

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


深入淺出畫圖講解React Diff原理【實踐】

新內容為文本

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


深入淺出畫圖講解React Diff原理【實踐】

新內容為數組

使用案例:

<code>
function ArrayComponent() {

const [ showingA, setShowingA ] = useState( true )

useEffect( () => {

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

} )

return showingA ?


​ A

​ B

:


​ C

​ D



}

ReactDOM.render( <arraycomponent>, document.getElementById('container') )
/<code>
深入淺出畫圖講解React Diff原理【實踐】

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

深入淺出畫圖講解React Diff原理【實踐】

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

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

...

i++

oldFiber = oldFiber.sibling
}/<code>

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

深入淺出畫圖講解React Diff原理【實踐】

updateSlot(...):

深入淺出畫圖講解React Diff原理【實踐】

因為newChild的類型為object, 所以:

深入淺出畫圖講解React Diff原理【實踐】

updateElement(...):

深入淺出畫圖講解React Diff原理【實踐】

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

  • 若新舊內容元素類型一致,則克隆舊fiber,結合新內容生成新的fiber
  • 若不一致,則基於新內容創建新的fiber。

同理,updateTextNode(...):

深入淺出畫圖講解React Diff原理【實踐】

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

  • 若舊內容fiber的標籤不是HostText,則基於新內容文本創建新的fiber
  • 若是HostText, 則克隆舊fiber,結合新內容文本生成新的fiber

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

深入淺出畫圖講解React Diff原理【實踐】

因為新舊內容數組的長度一致,所以直接返回第一個新的fiber。然後同上,React將新的fiber設為父fiber的child。

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

編寫類似案例。

若新內容數組長度更短:

深入淺出畫圖講解React Diff原理【實踐】

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

若新內容數組長度更長:

深入淺出畫圖講解React Diff原理【實踐】

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

總結

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

Diff的三種情況:

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

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

然後給舊內容fiber添加副作用替換標籤,或者給舊內容fiber及其所有相鄰元素添加副作用刪除標籤。

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

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

轉發鏈接:https://segmentfault.com/a/1190000022311760


分享到:


相關文章: