轉發鏈接:https://segmentfault.com/a/1190000022311760
時隔2年,重新看React源碼,很多以前不理解的內容現在都懂了。本文將用實際案例結合相關React源碼,集中討論React Diff原理。使用當前最新React版本:16.13.1。
另外,今年將寫一個“搞懂React源碼系列”,把React最核心內容用最通俗易懂地方式講清楚。2020年搞懂React源碼系列:
React Diff原理React 調度原理搭建閱讀React源碼環境-支持所有版本斷點調試React Hooks原理
歡迎Star和訂閱我的博客。
在討論Diff算法前,有必要先介紹React Fiber,因為React源碼中各種實現都是基於Fiber,包括Diff算法。當然,熟悉React Fiber的朋友可跳過Fiber介紹。
Fiber簡介
Fiber並不複雜,但如果要全面理解,還是得花好一段時間。本文主題是diff原理,所以這裡僅簡單介紹下Fiber。
Fiber是一個抽象的節點對象,每個對象可能有子Fiber(child)和相鄰Fiber(child)和父Fiber(return),React使用鏈表的形式將所有Fiber節點連接,形成鏈表樹。
Fiber還有副作用標籤(effectTag),比如替換Placement(替換)和Deletion(刪除),用於之後更新DOM。
值得注意的是,React diff中,除了fiber,還用到了基礎的React元素對象(如: 將
Diff 過程
React源碼中,關於diff要從reconcileChildren(...)說起。
總流程:
流程圖中, 顯示源碼中用到的函數名,省略複雜參數。“新內容”即被比較的新內容,它可能是三種類型:
對象: 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。左側調試面板可看到對應變量的類型。
此處:
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元素時類似:
新內容為數組
使用案例:
<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>
若新內容為數組,需reconcileChildrenArray(...):
for循環遍歷新內容數組,偽代碼(用於理解):
<code>for ( let i = 0, oldFiber; i < newArray.length; ) {
...
i++
oldFiber = oldFiber.sibling
}/<code>
遍歷每個新內容數組元素時:
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(替換)。
總結
通過React源碼研究diff算法時,僅調試分析
Diff的三種情況:
新內容為React元素新內容為文本新內容為數組Diff時若比較結果相同,則複用舊內容Fiber,結合新內容生成新Fiber;若不同,僅通過新內容創建新fiber。
然後給舊內容fiber添加副作用替換標籤,或者給舊內容fiber及其所有相鄰元素添加副作用刪除標籤。
最後將新的(第一個)fiber設為父fiber的child。
感謝你花時間閱讀這篇文章。如果你喜歡這篇文章,歡迎點贊、收藏和分享,讓更多的人看到這篇文章,這也是對我最大的鼓勵和支持!
轉發鏈接:https://segmentfault.com/a/1190000022311760