剖析 React 原始碼:render 流程(一)

yck發表於2019-05-05

這是我的剖析 React 原始碼的第二篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀原始碼。

現在請大家開啟 我的程式碼 並定位到 react-dom 資料夾下的 src 中的 ReactDOM.js 檔案,今天的內容會從這裡開始。

render

想必大家在寫 React 專案的時候都寫過類似的程式碼

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

這句程式碼告訴了 React 應用我們想在容器中渲染出一個元件,這通常也是一個 React 應用的入口程式碼,接下來我們就來梳理整個 render 的流程,並且會分為幾篇文章來講解,因為流程實在太長了。

首先請大家先定位到 ReactDOM.js 檔案的第 702 行程式碼,開始今天的旅程。

剖析 React 原始碼:render 流程(一)

這部分程式碼其實沒啥好說的,唯一需要注意的是在呼叫 legacyRenderSubtreeIntoContainer 函式時寫死了第四個引數 forceHydratefalse。這個引數為 true 時表明了是服務端渲染,因為我們分析的是客戶端渲染,因此後面有關這部分的內容也不會再展開。

接下來進入 legacyRenderSubtreeIntoContainer 函式中,這部分程式碼分為兩塊來講。第一部分是沒有 root 之前我們首先需要建立一個 root(對應這篇文章),第二部分是有 root 之後的渲染流程(對應接下來的文章)。

剖析 React 原始碼:render 流程(一)

一開始進來函式的時候肯定是沒有 root 的,因此我們需要去建立一個 root,大家可以發現這個 root 物件同樣也被掛載在了 container._reactRootContainer 上,也就是我們的 DOM 容器上。 如果你手邊有 React 專案的話,在控制檯鍵入如下程式碼就可以看到這個 root 物件了。

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

剖析 React 原始碼:render 流程(一)

大家可以看到 rootReactRoot 建構函式構造出來的,並且內部有一個 _internalRoot 物件,這個物件是本文接下來要重點介紹的 fiber 物件,接下來我們就來一窺究竟吧。

剖析 React 原始碼:render 流程(一)

首先還是和上文中提到的 forceHydrate 屬性相關的內容,不需要管這部分,反正 shouldHydrate 肯定為 false

接下來是將容器內部的節點全部移除,一般來說我們都是這樣寫一個容器的的

<div id='root'></div>
複製程式碼

這樣的形式肯定就不需要去移除子節點了,這也側面說明了一點那就是容器內部不要含有任何的子節點。一是肯定會被移除掉,二來還要進行 DOM 操作,可能還會涉及到重繪迴流等等。

最後就是建立了一個 ReactRoot 物件並返回。接下來的內容中我們會看到好幾個 root,可能會有點繞。

剖析 React 原始碼:render 流程(一)

ReactRoot 建構函式內部就進行了一步操作,那就是建立了一個 FiberRoot 物件,並掛載到了 _internalRoot 上。和 DOM 樹一樣,fiber 也會構建出一個樹結構(每個 DOM 節點一定對應著一個 fiber 物件),FiberRoot 就是整個 fiber 樹的根節點,接下來的內容裡我們將學習到關於 fiber 相關的內容。這裡提及一點,fiber 和 Fiber 是兩個不一樣的東西,前者代表著資料結構,後者代表著新的架構。

剖析 React 原始碼:render 流程(一)

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 樹應該長這樣

剖析 React 原始碼:render 流程(一)

從圖中我們可以看到,每個元件或者 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,這也是效能優化裡的一種做法,有興趣的同學可以自行查詢資料。

總結

以上就是本文的全部內容了,最後通過一張流程圖總結一下這篇文章的內容。

剖析 React 原始碼:render 流程(一)

最後

閱讀原始碼是一個很枯燥的過程,但是收益也是巨大的。如果你在閱讀的過程中有任何的問題,都歡迎你在評論區與我交流。

另外寫這系列是個很耗時的工程,需要維護程式碼註釋,還得把文章寫得儘量讓讀者看懂,最後還得配上畫圖,如果你覺得文章看著還行,就請不要吝嗇你的點贊。

下一篇文章還是 render 流程相關的內容。

最後,覺得內容有幫助可以關注下我的公眾號 「前端真好玩」咯,會有很多好東西等著你。

剖析 React 原始碼:render 流程(一)

相關文章