這是我的剖析 React 原始碼的第二篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀原始碼。
現在請大家開啟 我的程式碼 並定位到 react-dom 資料夾下的 src 中的 ReactDOM.js 檔案,今天的內容會從這裡開始。
render
想必大家在寫 React 專案的時候都寫過類似的程式碼
ReactDOM.render(<APP />, document.getElementById('root')
複製程式碼
這句程式碼告訴了 React 應用我們想在容器中渲染出一個元件,這通常也是一個 React 應用的入口程式碼,接下來我們就來梳理整個 render
的流程,並且會分為幾篇文章來講解,因為流程實在太長了。
首先請大家先定位到 ReactDOM.js 檔案的第 702 行程式碼,開始今天的旅程。
這部分程式碼其實沒啥好說的,唯一需要注意的是在呼叫 legacyRenderSubtreeIntoContainer
函式時寫死了第四個引數 forceHydrate
為 false
。這個引數為 true
時表明了是服務端渲染,因為我們分析的是客戶端渲染,因此後面有關這部分的內容也不會再展開。
接下來進入 legacyRenderSubtreeIntoContainer
函式中,這部分程式碼分為兩塊來講。第一部分是沒有 root
之前我們首先需要建立一個 root
(對應這篇文章),第二部分是有 root
之後的渲染流程(對應接下來的文章)。
一開始進來函式的時候肯定是沒有 root
的,因此我們需要去建立一個 root
,大家可以發現這個 root
物件同樣也被掛載在了 container._reactRootContainer
上,也就是我們的 DOM 容器上。
如果你手邊有 React 專案的話,在控制檯鍵入如下程式碼就可以看到這個 root
物件了。
document.querySelector('#root')._reactRootContainer
複製程式碼
大家可以看到 root
是 ReactRoot
建構函式構造出來的,並且內部有一個 _internalRoot
物件,這個物件是本文接下來要重點介紹的 fiber
物件,接下來我們就來一窺究竟吧。
首先還是和上文中提到的 forceHydrate
屬性相關的內容,不需要管這部分,反正 shouldHydrate
肯定為 false
。
接下來是將容器內部的節點全部移除,一般來說我們都是這樣寫一個容器的的
<div id='root'></div>
複製程式碼
這樣的形式肯定就不需要去移除子節點了,這也側面說明了一點那就是容器內部不要含有任何的子節點。一是肯定會被移除掉,二來還要進行 DOM 操作,可能還會涉及到重繪迴流等等。
最後就是建立了一個 ReactRoot
物件並返回。接下來的內容中我們會看到好幾個 root
,可能會有點繞。
在 ReactRoot
建構函式內部就進行了一步操作,那就是建立了一個 FiberRoot
物件,並掛載到了 _internalRoot
上。和 DOM 樹一樣,fiber
也會構建出一個樹結構(每個 DOM 節點一定對應著一個 fiber
物件),FiberRoot
就是整個 fiber
樹的根節點,接下來的內容裡我們將學習到關於 fiber
相關的內容。這裡提及一點,fiber
和 Fiber 是兩個不一樣的東西,前者代表著資料結構,後者代表著新的架構。
在 createFiberRoot
函式內部,分別建立了兩個 root
,一個 root
叫做 FiberRoot
,另一個 root
叫做 RootFiber
,並且它們兩者還是相互引用的。
這兩個物件內部擁有著數十個屬性,現在我們沒有必要一一去了解它們各自有什麼用處,在當下只需要瞭解少部分屬性即可,其他的屬性我們會在以後的文章中瞭解到它們的用處。
對於 FiberRoot
物件來說,我們現在只需要瞭解兩個屬性,分別是 containerInfo
及 current
。前者代表著容器資訊,也就是我們的 document.querySelector('#root')
;後者指向 RootFiber
。
對於 RootFiber
物件來說,我們需要了解的屬性稍微多點
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
this.stateNode = null;
this.return = null;
this.child = null;
this.sibling = null;
this.effectTag = NoEffect;
this.alternate = null;
}
複製程式碼
stateNode
上文中已經講過了,這裡就不再贅述。
return
、child
、sibling
這三個屬性很重要,它們是構成 fiber
樹的主體資料結構。fiber
樹其實是一個單連結串列樹結構,return
及 child
分別對應著樹的父子節點,並且父節點只有一個 child
指向它的第一個子節點,即便是父節點有好多個子節點。那麼多個子節點如何連線起來呢?答案是 sibling
,每個子節點都有一個 sibling
屬性指向著下一個子節點,都有一個 return
屬性指向著父節點。這麼說可能有點繞,我們通過圖來了解一下這個 fiber
樹的結構。
const APP = () => (
<div>
<span></span>
<span></span>
</div>
)
ReactDom.render(<APP/>, document.querySelector('#root'))
複製程式碼
假如說我們需要渲染出以上元件,那麼它們對應的 fiber
樹應該長這樣
從圖中我們可以看到,每個元件或者 DOM 節點都會對應著一個 fiber
物件。另外你手邊有 React 專案的話,也可以在控制檯輸入如下程式碼,檢視 fiber
樹的整個結構。
// 對應著 FiberRoot
const fiber = document.querySelector('#root')._reactRootContainer._internalRoot
複製程式碼
另外兩個屬性在本文中雖然用不上,但是看原始碼的時候筆者覺得很有意思,就打算拿出來說一下。
在說 effectTag
之前,我們先來了解下啥是 effect
,簡單來說就是 DOM 的一些操作,比如增刪改,那麼 effectTag
就是來記錄所有的 effect 的,但是這個記錄是通過位運算來實現的,這裡 是 effectTag
相關的二進位制內容。
如果我們想新增一個 effect
的話,可以這樣寫 effectTag |= Update
;如果我們想刪除一個 effect
的話,可以這樣寫 effectTag &= ~Update
。
最後是 alternate
屬性。其實在一個 React 應用中,通常來說都有兩個 fiebr
樹,一個叫做 old tree,另一個叫做 workInProgress tree。前者對應著已經渲染好的 DOM 樹,後者是正在執行更新中的 fiber tree,還能便於中斷後恢復。兩棵樹的節點互相引用,便於共享一些內部的屬性,減少記憶體的開銷。畢竟前文說過每個元件或 DOM 都會對應著一個 fiber
物件,應用很大的話組成的 fiber
樹也會很大,如果兩棵樹都是各自把一些相同的屬性建立一遍的話,會損失不少的記憶體空間及效能。
當更新結束以後,workInProgress tree 會將 old tree 替換掉,這種做法稱之為 double buffering,這也是效能優化裡的一種做法,有興趣的同學可以自行查詢資料。
總結
以上就是本文的全部內容了,最後通過一張流程圖總結一下這篇文章的內容。
最後
閱讀原始碼是一個很枯燥的過程,但是收益也是巨大的。如果你在閱讀的過程中有任何的問題,都歡迎你在評論區與我交流。
另外寫這系列是個很耗時的工程,需要維護程式碼註釋,還得把文章寫得儘量讓讀者看懂,最後還得配上畫圖,如果你覺得文章看著還行,就請不要吝嗇你的點贊。
下一篇文章還是 render 流程相關的內容。
最後,覺得內容有幫助可以關注下我的公眾號 「前端真好玩」咯,會有很多好東西等著你。