剖析 React 源碼:render 流程(一)

這是個人剖析 React 源碼的第二篇文章,若是你沒有閱讀過以前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。html

文章相關資料

如今請你們打開 個人代碼 並定位到 react-dom 文件夾下的 src 中的 ReactDOM.js 文件,今天的內容會從這裏開始。前端

render

想必你們在寫 React 項目的時候都寫過相似的代碼react

ReactDOM.render(<APP />, document.getElementById('root') 複製代碼

這句代碼告訴了 React 應用咱們想在容器中渲染出一個組件,這一般也是一個 React 應用的入口代碼,接下來咱們就來梳理整個 render 的流程,而且會分爲幾篇文章來說解,由於流程實在太長了。git

首先請你們先定位到 ReactDOM.js 文件的第 702 行代碼,開始今天的旅程。github

這部分代碼其實沒啥好說的,惟一須要注意的是在調用 legacyRenderSubtreeIntoContainer 函數時寫死了第四個參數 forceHydratefalse。這個參數爲 true 時代表了是服務端渲染,由於咱們分析的是客戶端渲染,所以後面有關這部分的內容也不會再展開。性能優化

接下來進入 legacyRenderSubtreeIntoContainer 函數中,這部分代碼分爲兩塊來說。第一部分是沒有 root 以前咱們首先須要建立一個 root(對應這篇文章),第二部分是有 root 以後的渲染流程(對應接下來的文章)。數據結構

一開始進來函數的時候確定是沒有 root 的,所以咱們須要去建立一個 root,你們能夠發現這個 root 對象一樣也被掛載在了 container._reactRootContainer 上,也就是咱們的 DOM 容器上。 若是你手邊有 React 項目的話,在控制檯鍵入以下代碼就能夠看到這個 root 對象了。架構

document.querySelector('#root')._reactRootContainer
複製代碼

你們能夠看到 rootReactRoot 構造函數構造出來的,而且內部有一個 _internalRoot 對象,這個對象是本文接下來要重點介紹的 fiber 對象,接下來咱們就來一窺究竟吧。dom

首先仍是和上文中提到的 forceHydrate 屬性相關的內容,不須要管這部分,反正 shouldHydrate 確定爲 falseide

接下來是將容器內部的節點所有移除,通常來講咱們都是這樣寫一個容器的的

<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 對象來講,咱們如今只須要了解兩個屬性,分別是 containerInfocurrent。前者表明着容器信息,也就是咱們的 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 上文中已經講過了,這裏就再也不贅述。

returnchildsibling 這三個屬性很重要,它們是構成 fiber 樹的主體數據結構。fiber 樹實際上是一個單鏈表樹結構,returnchild 分別對應着樹的父子節點,而且父節點只有一個 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 流程相關的內容。

最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。

相關文章
相關標籤/搜索