今天想了比較久的時間,準備開啓這一系列的文章,旨在對 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
)(見下圖),我們的渲染流程解析宣告完成!
最後也是按照慣例,用一張流程圖來梳理我們的整個渲染過程。
在本篇文章主要是解析 React
的整個渲染過程,對 React
的整體結構和工作流程有了個初步的瞭解,下一章將圍繞着 Component
的生命週期與 setState
進行解析,如有需要可以關注專欄後續更新。