時隔2年,從新看React源碼,不少之前不理解的內容如今都懂了。本文將用實際案例結合相關React源碼,集中討論React Diff原理。使用當前最新React版本:16.13.1
。html
另外,今年將寫一個「搞懂React源碼系列」,把React最核心內容用最通俗易懂地方式講清楚。2020年搞懂React源碼系列:react
React Diff原理git
React 調度原理github
搭建閱讀React源碼環境-支持全部版本斷點調試算法
React Hooks原理數組
歡迎Star和訂閱個人博客。微信
在討論Diff算法前,有必要先介紹React Fiber,由於React源碼中各類實現都是基於Fiber,包括Diff算法。固然,熟悉React 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' } }
)。
React源碼中,關於diff要從reconcileChildren(...)
提及。
總流程:
流程圖中, 顯示源碼中用到的函數名,省略複雜參數。「新內容」即被比較的新內容,它多是三種類型:
對象: 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的三種狀況:
Diff時若比較結果相同,則複用舊內容Fiber,結合新內容生成新Fiber;若不一樣,僅經過新內容建立新fiber。
而後給舊內容fiber添加反作用替換標籤,或者給舊內容fiber及其全部相鄰元素添加反作用刪除標籤。
最後將新的(第一個)fiber設爲父fiber的child。
The how and why on React’s usage of linked list in Fiber to walk the component’s tree: medium.com/react-in-de…
[譯]深刻React fiber架構及源碼: zhuanlan.zhihu.com/p/57346388
Inside Fiber: in-depth overview of the new reconciliation algorithm in React: medium.com/react-in-de…
感謝你花時間閱讀這篇文章。若是你喜歡這篇文章,歡迎點贊、收藏和分享,讓更多的人看到這篇文章,這也是對我最大的鼓勵和支持!
歡迎經過微信(掃描下方二維碼)或Github訂閱個人博客。