今天想了比較久的時間,準備開啟這一系列的文章,旨在對 React 系列的源碼進行深度解析,其中包含但不限于 react、react-dom、react-router... 等一系列優(yōu)秀的 React 系列框架,最后再一一實現(xiàn)這些框架的簡易版本。
本篇文章將會是對 react 和 react-dom 渲染過程源碼的深度解析,我們將從官方 API 以及一些簡易 Demo 來進入 react 的內(nèi)部世界,探討其中奧妙。
本文解析的
react版本為v16.13.0,是我 fork 的官方倉庫,源碼地址。
結(jié)構(gòu)剖析

我們先從最基礎的結(jié)構(gòu)開始解析,從上面這張圖來看看。我們創(chuàng)建了一個 App 類,繼承于 React.Component 類,在 render 生命周期函數(shù)中返回了一個 jsx 格式的 html 標簽集合。我們打開控制臺,查看創(chuàng)建的實例(如下圖):

我們逐一分析其中比較關鍵的屬性:
| 字段 | 解釋 |
|---|---|
props |
把 Component 組件比作函數(shù),props 就是函數(shù)的入?yún)?/td>
|
context |
context 就是在組件樹之間共享的信息 |
refs |
訪問原生 DOM 元素的集合 |
updater |
負責 Component 組件狀態(tài)的更新 |
_reactInternalFiber |
App 實例對應的 FiberNode
|
一個 Component 實例的大致結(jié)構(gòu)我們就解析完了,我們現(xiàn)在需要由內(nèi)到外的繼續(xù)解析 Component 內(nèi)部結(jié)構(gòu)以及實現(xiàn)。
我們現(xiàn)在來看看 render 方法內(nèi)部, 第 7 行 的內(nèi)容屬于 jsx 語法,是一種 html 語法格式類似的高級模板語法。這一段我們需要借鑒一下官方的一張圖來進行解釋:

從上圖可以得知,jsx 語法都會被編譯成 React.createElement 函數(shù),標簽屬性以及標簽內(nèi)容都會編譯成對應的入?yún)?,由此可知我們所寫?第 7 行 代碼在編譯后將會變成如下代碼:
React.createElement("section", {}, "Hello World");
而 React.createElement 所創(chuàng)建的對象就是 虛擬 DOM 樹,那么內(nèi)部創(chuàng)建的工作流程是什么樣的?帶著這個問題,我們進入下一個章節(jié)。
React.createElement
我們剛才得知 jsx 語法將會被編譯成 React.createElement 函數(shù)調(diào)用,而這個函數(shù)屬于 React 對象上的一個方法,現(xiàn)在我們就可以開始進入到源碼解析,查看內(nèi)部實現(xiàn)。


上圖就是 React.createElement,我們先看最后返回的結(jié)果是 ReactElement 函數(shù)的執(zhí)行結(jié)果,該函數(shù)最后返回的是一個 React Element 對象(后面會提到)。
所以 React.createElement 其實是一個工廠函數(shù),用于創(chuàng)建 React Element 對象,我們再來看看這個工廠函數(shù)主要做了哪些工作。
-
11-29 行:收集了config中的一些字段,并且將其他非內(nèi)置字段添加到props對象中; -
31-40 行:將入?yún)⒅械?children參數(shù)掛載到props的children字段中;(本示例中"Hello World"就是一個 “children”) -
42-49 行:收集組件(type可能是字符串也有可能是Component實例,例如<section />和<App />)中設置的defaultProps屬性;
在完成一系列的初始化工作后,進入了 ReactElement 的創(chuàng)建工作(見下圖)。

ReactElement 函數(shù)就比較一目了然了,返回了一個 element(React Element) 對象。React Element 對象其實就是一棵虛擬 DOM 樹($$typeof 字段表示了這是一個 React Element 類型),包含了標簽和屬性(attribute)信息。Component 執(zhí)行 render 函數(shù)得到 虛擬 DOM 樹,再通過 react-dom 將其包裝成 FiberNode,然后被解析成 真實 DOM 樹 后渲染在頁面中(對應的容器內(nèi)),這個我們后續(xù)再詳細解析,這里就不展開了。
我們最后對 React Element 的創(chuàng)建過程畫一個流程圖來加深理解。(見下圖)

React.Component
我們接下來要對 React.Component 進行進一步的解析,看看 Component 整體的運行邏輯以及是如何使用 React.Element 的。

Component 屬于一個構(gòu)造函數(shù)(見上圖),Component 定義了幾個屬性,分別是 props、context、refs、updater,這些屬性在之前已經(jīng)解釋過,這里不再復述。這里需要注意的是 Component 中的兩個方法 setState 和 forceUpdate,調(diào)用的都是內(nèi)部 updater 的方法進行事件通知,將數(shù)據(jù)和 UI 更新的任務交給了內(nèi)部的 updater 去處理,符合 單一職責設計原則。
到這里,Component 類的結(jié)構(gòu)已經(jīng)解析完成了。什么,這就解析完成了?生命周期函數(shù)呢?渲染過程呢?一個都還沒有看到啊。別著急,由于 React 內(nèi)部的職責劃分與不同平臺實現(xiàn),所以這部分根據(jù)不同平臺的實現(xiàn)被放在了 react-dom 或 react-native 中。我們接下來就對我們常用的瀏覽器端,react-dom 中渲染過程以及對組件生命周期的處理進行詳細的梳理。在此之前,放張圖對本章的 Component 進行小結(jié)。

渲染過程(react-dom)
render 函數(shù)
在解析完了 React.Element 和 React.Component 之后,可能很多人只是了解到了基礎結(jié)構(gòu)體的創(chuàng)建,還是感覺云里霧里。現(xiàn)在我們來理一理 react-dom 的整個渲染過程以及組件生命周期,從 constructor 組件的創(chuàng)建到 componentDidMount() 組件的掛載,最后再畫一個流程圖來進行總結(jié)。
react 本身只是一些基礎類的創(chuàng)建,比如 React.Element 和 React.Component,而后續(xù)的流程則根據(jù)不同的平臺有不同的實現(xiàn)。我們這里以我們常用的瀏覽器環(huán)境為例,調(diào)用的是 ReactDOM.render() 方法(見下圖),我們現(xiàn)在就來對這個方法的渲染過程做一個詳細解析。


從上圖可以看出,render 函數(shù)返回 legacyRenderSubtreeIntoContainer 函數(shù)的調(diào)用,而該函數(shù)最終返回的結(jié)果是 Component 實例(也就是 App 組件,見下圖)。

FiberNode
我們來看看 render 函數(shù)內(nèi)部調(diào)用的 legacyRenderSubtreeIntoContainer 函數(shù)(見下圖)

在 legacyRenderSubtreeIntoContainer 中的 第 28 行,就是 FiberNode 樹 的創(chuàng)建過程。
FiberNode 由內(nèi)部的 createFiber 函數(shù)進行創(chuàng)建(見下圖)。(這也是 React 在 16 版本后作出的巨大更新,這個后面我們再展開說)。

FiberNode 被創(chuàng)建后掛載在了 FiberRoot.current 上。最后,App 組件作為根組件實例被返回,而接下來的渲染過程由 FiberNode 接管。
我們畫一個流程圖來幫助理解(見下圖)。

從上圖可以看出,我們的 React Element 作為 render 函數(shù)的入?yún)ⅲ瑒?chuàng)建了一個 FiberNode 實例,也就是 FiberRoot.current,而后續(xù)的渲染過程都由這個根 FiberNode 接管,包括所有的生命周期。
遞歸構(gòu)建 FiberNode 樹
在構(gòu)建完了根 FiberNode 實例后,第 40 行 調(diào)用了 updaterContainer 函數(shù)開始構(gòu)建整棵 FiberNode 樹以及完成 DOM 渲染(見下圖)。


updaterContainer 是一個比較關鍵的函數(shù),我們來解析一下這個函數(shù)做了什么:
-
第 8~14 行:React內(nèi)部的更新任務設置了優(yōu)先級大小,優(yōu)先級較高的更新任務將會中斷優(yōu)先級較低的更新任務。React設置了ExpirationTime任務過期時間,如果時間到期后任務仍未執(zhí)行(一直被打斷),則會強制執(zhí)行該更新任務。同時,React內(nèi)部也會將過期時間相近的更新任務合并成一個(批量)更新任務,從而達到批量更新減少消耗的效果。(React setState “異步” 更新原理) -
第 16~21 行:從父組件中收集context屬性(由于這里是root組件,所以父組件為空)。 -
第 23~31 行:構(gòu)建更新隊列,第 24 行將Element實例(見下圖 1)掛載在update對象上,第 31 行將更新隊列(updateQueue) 掛載在FiberNode實例(見下圖 2)。


-
第 32 行:內(nèi)部開始遞歸調(diào)度,創(chuàng)建FiberNode樹。創(chuàng)建一個工作節(jié)點快照workInProgress(初始值是根FiberNode),圍繞著workInProgress對updateQueue展開構(gòu)建工作(見下圖);
根據(jù)
updateQueue更新節(jié)點(performUnitOfWork將返回workInProgress.child,直到所有節(jié)點遍歷完成)

創(chuàng)建 FiberNode 子節(jié)點
進入 performUnitOfWork 函數(shù)內(nèi)部,我們省略掉一系列目前不需要關注的函數(shù),首先進入到 beginWork 函數(shù)(見下圖)。

beginWork 函數(shù)會根據(jù) props 和 context 是否改變(第 12~15 行)、當前當前節(jié)點優(yōu)先級是否高于正在更新的節(jié)點優(yōu)先級(第 17 行)這兩項來決定當前節(jié)點是否需要更新。
然后根據(jù)節(jié)點的標簽類型(tag),調(diào)用不同的函數(shù)進行內(nèi)部狀態(tài)更新。(見下圖)

Root(FiberNode) 節(jié)點更新 - updateHostRoot
我們第一次進入是 root 節(jié)點,所以進入到 updateHostRoot 函數(shù)內(nèi)部邏輯進行處理。(見下圖)


按照慣例,我們逐行解析函數(shù)所做的事情:
-
第 2 行:將一系列有用的信息推入內(nèi)部棧(其中包括#app實例、context信息等等)。 -
第 5~7 行:收集節(jié)點新的props屬性和舊的state、children屬性。 -
第 8 行:淺復制更新隊列,防止引用屬性互相影響; -
第 9 行:執(zhí)行更新隊列,主要的任務是將React.Element添加到Fiber的memoizedState和updateQueue更新隊列中(見下圖);

-
第 36~45 行:對上一步的memoizedState中的element進行進一步的處理,將其封裝成FiberNode然后掛載在workInProgress(當前工作節(jié)點快照).child屬性上,最后將該child返回。
到這一步,FiberNode 樹的第一個節(jié)點就已經(jīng)構(gòu)建完成并掛載,我們來畫一張流程圖進行梳理(下圖)。

App Component(FiberNode) 更新流程 - updateClassComponent
接下來就是對子節(jié)點的依次更新流程(見下圖),也就是 App Component 對應的 FiberNode。依然是 beginWork 函數(shù),在 第 232~246 行 調(diào)用我們的 App Component 節(jié)點的更新流程。

constructClassInstance
在 updateClassComponent 函數(shù)中,有三個關鍵函數(shù),第一個就是 constructClassInstance。


在 constructClassInstance 函數(shù)中(見上圖 1):
-
第 96 行創(chuàng)建App Component實例。 -
第 101 行將實例掛載在workInProgress的stateNode屬性中(件上圖 2) -
第 107 行最后返回該實例。
mountClassInstance
在 constructClassInstance 執(zhí)行完成后,接下來執(zhí)行第二個關鍵函數(shù) mountClassInstance。


mountClassInstance 函數(shù)中對 Component 實例進行掛載的一些初始化工作(見上圖)。我們從上圖可以看出,到了這里就開始了 Component 的生命周期鉤子邏輯。
在初始化實例的一些基礎屬性后,第 136~145 行執(zhí)行了 Component 的第一個生命周期鉤子,也就是 getDerivedStateFromProps(見上圖),它使用返回的對象來更新 state。
而緊隨其后(見下圖) 第 153 行 觸發(fā)了第二個生命周期鉤子 componentWillMount,主要用于在掛載前執(zhí)行一些操作。


finishClassComponent
在實例創(chuàng)建完成并且調(diào)用了上面兩個生命周期鉤子后,進入到最后一個關鍵函數(shù) finishClassComponent。

在 finishClassComponent 中 render 函數(shù)(見上圖)。而 render 函數(shù)執(zhí)行返回的就是 React.Element(虛擬 DOM 樹)(下圖 1),最后將其包裝成 FiberNode 后返回(下圖 2)后進入進入 workLoopSync 流程。


React Element(FiberNode) 更新流程 - updateHostComponent
還是 beginWork 函數(shù)(見下圖),進入 updateHostComponent 進行 React Element(FiberNode) 組件更新階段。


在 第 13 行 會對組件的 children 類型進行判斷,判斷是否為純文本內(nèi)容,我們在此處就是純文本(section 標簽內(nèi)的 Hello World 文本),隨后 nextChildren 就將被置空。
到這里,nextChildren 已經(jīng)為空,完整的 FiberNode 樹就已經(jīng)構(gòu)建完成。beginWork 結(jié)束,接下來進入到新的流程。
創(chuàng)建 真實 DOM 樹
在結(jié)束了 beginWork 流程后,將調(diào)用 createInstance 函數(shù)創(chuàng)建 真實 DOM 樹(見下圖)。

在 createInstance 內(nèi)部調(diào)用了 createElement 函數(shù)創(chuàng)建了 真實 DOM 節(jié)點(見下圖 1),然后通過遞歸遍歷 props 中的屬性(包括 children)構(gòu)建了一棵 真實 DOM 樹(見下圖 2)


通過調(diào)用 createInstance 方法創(chuàng)建真實 DOM(此時還沒有插入到文檔中)后,然后將 DOM 樹 對象掛載到 FiberNode 的 stateNode 屬性上(見下圖)。

在 真實 DOM 樹構(gòu)建完成后,并且此時 workInProgress.child 也為 null,本次 workLoopSync 流程將在此結(jié)束,接下來進入到 finishSyncRender 函數(shù),進行節(jié)點的渲染工作。
渲染真實 DOM
react-dom 將在回調(diào)函數(shù)內(nèi)部將調(diào)用 insertOrAppendPlacementNodeIntoContainer 方法對 FiberNode 進行遍歷。(見下圖)

由上圖可知該函數(shù)會對 Host 節(jié)點(帶有 html tag 結(jié)構(gòu)的節(jié)點)調(diào)用 appendChildToContainer 函數(shù)進行渲染,其他節(jié)點取其 child 值進行遞歸調(diào)用。
在 appendChildToContainer 函數(shù)內(nèi)部,通過 appendChild 將 FiberNode 上的 stateNode (我們在上一步創(chuàng)建好的 真實 DOM 樹)添加到 container(#app)中,然后調(diào)用 componentDidMount 生命周期鉤子函數(shù)。(見下圖)


到了這一步,頁面中就渲染了我們在 render 中設置的 jsx 語法標簽(Hello World)(見下圖),我們的渲染流程解析宣告完成!

最后也是按照慣例,用一張流程圖來梳理我們的整個渲染過程。