今天想了比較久的時間,準備開啟這一系列的文章,旨在對 React
系列的源碼進行深度解析,其中包含但不限于 react、react-dom、react-router...
等一系列優秀的 React
系列框架,最后再一一實現這些框架的簡易版本。
本篇文章將會是對 react 和 react-dom
渲染過程源碼的深度解析,我們將從官方 API 以及一些簡易 Demo 來進入 react
的內部世界,探討其中奧妙。
本文解析的
react
版本為v16.13.0
,是我 fork 的官方倉庫,源碼地址。
結構剖析
我們先從最基礎的結構開始解析,從上面這張圖來看看。我們創建了一個 App
類,繼承于 React.Component
類,在 render
生命周期函數中返回了一個 jsx
格式的 html
標簽集合。我們打開控制臺,查看創建的實例(如下圖):
我們逐一分析其中比較關鍵的屬性:
字段 | 解釋 |
---|---|
props |
把 Component 組件比作函數,props 就是函數的入參 |
context |
context 就是在組件樹之間共享的信息 |
refs |
訪問原生 DOM 元素的集合 |
updater |
負責 Component 組件狀態的更新 |
_reactInternalFiber |
App 實例對應的 FiberNode
|
一個 Component
實例的大致結構我們就解析完了,我們現在需要由內到外的繼續解析 Component
內部結構以及實現。
我們現在來看看 render
方法內部, 第 7 行
的內容屬于 jsx
語法,是一種 html
語法格式類似的高級模板語法。這一段我們需要借鑒一下官方的一張圖來進行解釋:
從上圖可以得知,jsx
語法都會被編譯成 React.createElement
函數,標簽屬性以及標簽內容都會編譯成對應的入參,由此可知我們所寫的 第 7 行
代碼在編譯后將會變成如下代碼:
React.createElement("section", {}, "Hello World");
而 React.createElement
所創建的對象就是 虛擬 DOM 樹
,那么內部創建的工作流程是什么樣的?帶著這個問題,我們進入下一個章節。
React.createElement
我們剛才得知 jsx
語法將會被編譯成 React.createElement
函數調用,而這個函數屬于 React
對象上的一個方法,現在我們就可以開始進入到源碼解析,查看內部實現。
上圖就是 React.createElement
,我們先看最后返回的結果是 ReactElement
函數的執行結果,該函數最后返回的是一個 React Element
對象(后面會提到)。
所以 React.createElement
其實是一個工廠函數,用于創建 React Element
對象,我們再來看看這個工廠函數主要做了哪些工作。
-
11-29 行
:收集了config
中的一些字段,并且將其他非內置字段添加到props
對象中; -
31-40 行
:將入參中的children
參數掛載到props
的children
字段中;(本示例中"Hello World"
就是一個 “children
”) -
42-49 行
:收集組件(type
可能是字符串也有可能是Component
實例,例如<section />
和<App />
)中設置的defaultProps
屬性;
在完成一系列的初始化工作后,進入了 ReactElement
的創建工作(見下圖)。
ReactElement
函數就比較一目了然了,返回了一個 element(React Element)
對象。React Element
對象其實就是一棵虛擬 DOM 樹($$typeof
字段表示了這是一個 React Element
類型),包含了標簽和屬性(attribute
)信息。Component
執行 render
函數得到 虛擬 DOM 樹
,再通過 react-dom
將其包裝成 FiberNode
,然后被解析成 真實 DOM 樹
后渲染在頁面中(對應的容器內),這個我們后續再詳細解析,這里就不展開了。
我們最后對 React Element
的創建過程畫一個流程圖來加深理解。(見下圖)
React.Component
我們接下來要對 React.Component
進行進一步的解析,看看 Component
整體的運行邏輯以及是如何使用 React.Element
的。
Component
屬于一個構造函數(見上圖),Component
定義了幾個屬性,分別是 props、context、refs、updater
,這些屬性在之前已經解釋過,這里不再復述。這里需要注意的是 Component
中的兩個方法 setState
和 forceUpdate
,調用的都是內部 updater
的方法進行事件通知,將數據和 UI 更新的任務交給了內部的 updater
去處理,符合 單一職責設計原則
。
到這里,Component
類的結構已經解析完成了。什么,這就解析完成了?生命周期函數呢?渲染過程呢?一個都還沒有看到啊。別著急,由于 React
內部的職責劃分與不同平臺實現,所以這部分根據不同平臺的實現被放在了 react-dom
或 react-native
中。我們接下來就對我們常用的瀏覽器端,react-dom
中渲染過程以及對組件生命周期的處理進行詳細的梳理。在此之前,放張圖對本章的 Component
進行小結。
渲染過程(react-dom
)
render
函數
在解析完了 React.Element
和 React.Component
之后,可能很多人只是了解到了基礎結構體的創建,還是感覺云里霧里。現在我們來理一理 react-dom
的整個渲染過程以及組件生命周期,從 constructor
組件的創建到 componentDidMount()
組件的掛載,最后再畫一個流程圖來進行總結。
react
本身只是一些基礎類的創建,比如 React.Element
和 React.Component
,而后續的流程則根據不同的平臺有不同的實現。我們這里以我們常用的瀏覽器環境為例,調用的是 ReactDOM.render()
方法(見下圖),我們現在就來對這個方法的渲染過程做一個詳細解析。
從上圖可以看出,render
函數返回 legacyRenderSubtreeIntoContainer
函數的調用,而該函數最終返回的結果是 Component
實例(也就是 App 組件,見下圖)。
FiberNode
我們來看看 render
函數內部調用的 legacyRenderSubtreeIntoContainer
函數(見下圖)
在 legacyRenderSubtreeIntoContainer
中的 第 28 行
,就是 FiberNode 樹
的創建過程。
FiberNode
由內部的 createFiber
函數進行創建(見下圖)。(這也是 React
在 16
版本后作出的巨大更新,這個后面我們再展開說)。
FiberNode
被創建后掛載在了 FiberRoot.current
上。最后,App
組件作為根組件實例被返回,而接下來的渲染過程由 FiberNode
接管。
我們畫一個流程圖來幫助理解(見下圖)。
從上圖可以看出,我們的 React Element
作為 render
函數的入參,創建了一個 FiberNode
實例,也就是 FiberRoot.current
,而后續的渲染過程都由這個根 FiberNode
接管,包括所有的生命周期。
遞歸構建 FiberNode 樹
在構建完了根 FiberNode
實例后,第 40 行
調用了 updaterContainer
函數開始構建整棵 FiberNode
樹以及完成 DOM
渲染(見下圖)。
updaterContainer
是一個比較關鍵的函數,我們來解析一下這個函數做了什么:
-
第 8~14 行
:React
內部的更新任務設置了優先級大小,優先級較高的更新任務將會中斷優先級較低的更新任務。React
設置了ExpirationTime
任務過期時間,如果時間到期后任務仍未執行(一直被打斷),則會強制執行該更新任務。同時,React
內部也會將過期時間相近的更新任務合并成一個(批量)更新任務,從而達到批量更新減少消耗的效果。(React setState “異步” 更新原理
) -
第 16~21 行
:從父組件中收集context
屬性(由于這里是root
組件,所以父組件為空)。 -
第 23~31 行
:構建更新隊列,第 24 行
將Element
實例(見下圖 1)掛載在update
對象上,第 31 行
將更新隊列(updateQueue
) 掛載在FiberNode
實例(見下圖 2)。
-
第 32 行
:內部開始遞歸調度,創建FiberNode
樹。創建一個工作節點快照workInProgress
(初始值是根FiberNode
),圍繞著workInProgress
對updateQueue
展開構建工作(見下圖);
根據
updateQueue
更新節點(performUnitOfWork
將返回workInProgress.child
,直到所有節點遍歷完成)
創建 FiberNode
子節點
進入 performUnitOfWork
函數內部,我們省略掉一系列目前不需要關注的函數,首先進入到 beginWork
函數(見下圖)。
beginWork
函數會根據 props
和 context
是否改變(第 12~15 行
)、當前當前節點優先級是否高于正在更新的節點優先級(第 17 行
)這兩項來決定當前節點是否需要更新。
然后根據節點的標簽類型(tag
),調用不同的函數進行內部狀態更新。(見下圖)
Root(FiberNode)
節點更新 - updateHostRoot
我們第一次進入是 root
節點,所以進入到 updateHostRoot
函數內部邏輯進行處理。(見下圖)
按照慣例,我們逐行解析函數所做的事情:
-
第 2 行
:將一系列有用的信息推入內部棧(其中包括#app
實例、context
信息等等)。 -
第 5~7 行
:收集節點新的props
屬性和舊的state、children
屬性。 -
第 8 行
:淺復制更新隊列,防止引用屬性互相影響; -
第 9 行
:執行更新隊列,主要的任務是將React.Element
添加到Fiber
的memoizedState
和updateQueue
更新隊列中(見下圖);
-
第 36~45 行
:對上一步的memoizedState
中的element
進行進一步的處理,將其封裝成FiberNode
然后掛載在workInProgress(當前工作節點快照).child
屬性上,最后將該child
返回。
到這一步,FiberNode
樹的第一個節點就已經構建完成并掛載,我們來畫一張流程圖進行梳理(下圖)。
App Component(FiberNode)
更新流程 - updateClassComponent
接下來就是對子節點的依次更新流程(見下圖),也就是 App Component
對應的 FiberNode
。依然是 beginWork
函數,在 第 232~246 行
調用我們的 App Component
節點的更新流程。
constructClassInstance
在 updateClassComponent
函數中,有三個關鍵函數,第一個就是 constructClassInstance
。
在 constructClassInstance
函數中(見上圖 1):
-
第 96 行
創建App Component
實例。 -
第 101 行
將實例掛載在workInProgress
的stateNode
屬性中(件上圖 2) -
第 107 行
最后返回該實例。
mountClassInstance
在 constructClassInstance
執行完成后,接下來執行第二個關鍵函數 mountClassInstance
。
mountClassInstance
函數中對 Component
實例進行掛載的一些初始化工作(見上圖)。我們從上圖可以看出,到了這里就開始了 Component
的生命周期鉤子邏輯。
在初始化實例的一些基礎屬性后,第 136~145
行執行了 Component
的第一個生命周期鉤子,也就是 getDerivedStateFromProps(見上圖)
,它使用返回的對象來更新 state
。
而緊隨其后(見下圖) 第 153 行
觸發了第二個生命周期鉤子 componentWillMount
,主要用于在掛載前執行一些操作。
finishClassComponent
在實例創建完成并且調用了上面兩個生命周期鉤子后,進入到最后一個關鍵函數 finishClassComponent
。
在 finishClassComponent
中 render
函數(見上圖)。而 render
函數執行返回的就是 React.Element(虛擬 DOM 樹)
(下圖 1),最后將其包裝成 FiberNode
后返回(下圖 2)后進入進入 workLoopSync
流程。
React Element(FiberNode)
更新流程 - updateHostComponent
還是 beginWork
函數(見下圖),進入 updateHostComponent
進行 React Element(FiberNode)
組件更新階段。
在 第 13 行
會對組件的 children
類型進行判斷,判斷是否為純文本內容,我們在此處就是純文本(section
標簽內的 Hello World
文本),隨后 nextChildren
就將被置空。
到這里,nextChildren
已經為空,完整的 FiberNode
樹就已經構建完成。beginWork
結束,接下來進入到新的流程。
創建 真實 DOM 樹
在結束了 beginWork
流程后,將調用 createInstance
函數創建 真實 DOM 樹
(見下圖)。
在 createInstance
內部調用了 createElement
函數創建了 真實 DOM 節點
(見下圖 1),然后通過遞歸遍歷 props
中的屬性(包括 children
)構建了一棵 真實 DOM 樹
(見下圖 2)
通過調用 createInstance
方法創建真實 DOM(此時還沒有插入到文檔中)后,然后將 DOM 樹
對象掛載到 FiberNode
的 stateNode
屬性上(見下圖)。
在 真實 DOM 樹
構建完成后,并且此時 workInProgress.child
也為 null
,本次 workLoopSync
流程將在此結束,接下來進入到 finishSyncRender
函數,進行節點的渲染工作。
渲染真實 DOM
react-dom
將在回調函數內部將調用 insertOrAppendPlacementNodeIntoContainer
方法對 FiberNode
進行遍歷。(見下圖)
由上圖可知該函數會對 Host
節點(帶有 html tag
結構的節點)調用 appendChildToContainer
函數進行渲染,其他節點取其 child
值進行遞歸調用。
在 appendChildToContainer
函數內部,通過 appendChild
將 FiberNode
上的 stateNode
(我們在上一步創建好的 真實 DOM 樹
)添加到 container(#app)
中,然后調用 componentDidMount
生命周期鉤子函數。(見下圖)
到了這一步,頁面中就渲染了我們在 render
中設置的 jsx 語法標簽
(Hello World
)(見下圖),我們的渲染流程解析宣告完成!
最后也是按照慣例,用一張流程圖來梳理我們的整個渲染過程。