時隔2年,從新看React源碼,不少之前不理解的內容如今都懂了。本文將用實際案例結合相關React源碼,集中討論React Diff原理。使用當前最新React版本:16.13.1
。html
另外,今年將寫一個「搞懂React源碼系列」,把React最核心內容用最通俗易懂地方式講清楚。2020年搞懂React源碼系列:react
- React Diff原理
- React 調度原理
- 搭建閱讀React源碼環境-支持全部版本斷點調試
- React Hooks原理
歡迎Star和訂閱個人博客。git
在討論Diff算法前,有必要先介紹React Fiber,由於React源碼中各類實現都是基於Fiber,包括Diff算法。固然,熟悉React Fiber的朋友可跳過Fiber介紹。github
Fiber並不複雜,但若是要全面理解,仍是得花好一段時間。本文主題是diff原理,因此這裏僅簡單介紹下Fiber。算法
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' } }
)。架構
React源碼中,關於diff要從reconcileChildren(...)
提及。ide
總流程:函數
流程圖中, 顯示源碼中用到的函數名,省略複雜參數。「新內容」即被比較的新內容,它多是三種類型:
咱們先以新內容爲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
: 父級Fibercurrent.child
: 處於比較中的舊內容對應fibernextChildren
: 即處於比較中的新內容, 爲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(...)
核心邏輯一致:
同理,updateTextNode(...)
:
updateTextNode(...)
與reconcileSingleTextNode(...)
核心邏輯一致:
HostText
,則基於新內容文本建立新的fiberHostText
, 則克隆舊fiber,結合新內容文本生成新的fiber在本案例中,新內容數組for循環完成後:
由於新舊內容數組的長度一致,因此直接返回第一個新的fiber。而後同上,React將新的fiber設爲父fiber的child。
不過若新內容數組長度與舊內容fiber及其相鄰fiber的總個數不一致,React如何處理?
編寫相似案例。
若新內容數組長度更短:
React將刪除多餘的舊內容fiber的相鄰fiber。
若新內容數組長度更長:
React將遍歷多餘的新內容數組元素,基於新內容數組元素建立的新的fiber,並添加反作用標籤 Placement(替換)。
新內容爲數組時的diff流程總結:
經過React源碼研究diff算法時,僅調試分析相關代碼,能比較容易的得出答案。
Diff的三種狀況:
Diff時若比較結果相同,則複用舊內容Fiber,結合新內容生成新Fiber;若不一樣,僅經過新內容建立新fiber。
而後給舊內容fiber添加反作用替換標籤,或者給舊內容fiber及其全部相鄰元素添加反作用刪除標籤。
最後將新的(第一個)fiber設爲父fiber的child。
感謝你花時間閱讀這篇文章。若是你喜歡這篇文章,歡迎點贊、收藏和分享,讓更多的人看到這篇文章,這也是對我最大的鼓勵和支持!
歡迎經過微信(掃描下方二維碼)或Github訂閱個人博客。