萬字長文+圖文并茂+全面解析 React 源碼 - react render 篇

今天想了比較久的時間,準備開啟這一系列的文章,旨在對 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)剖析

React

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

React

我們逐一分析其中比較關鍵的屬性:

字段 解釋
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 語法格式類似的高級模板語法。這一段我們需要借鑒一下官方的一張圖來進行解釋:

React

從上圖可以得知,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

React

上圖就是 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ù)掛載到 propschildren 字段中;(本示例中 "Hello World" 就是一個 “children”)
  • 42-49 行:收集組件(type 可能是字符串也有可能是 Component 實例,例如 <section /><App />)中設置的 defaultProps 屬性;

在完成一系列的初始化工作后,進入了 ReactElement 的創(chuàng)建工作(見下圖)。

React

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

React.Component

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

React

Component 屬于一個構(gòu)造函數(shù)(見上圖),Component 定義了幾個屬性,分別是 props、context、refs、updater,這些屬性在之前已經(jīng)解釋過,這里不再復述。這里需要注意的是 Component 中的兩個方法 setStateforceUpdate,調(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-domreact-native 中。我們接下來就對我們常用的瀏覽器端,react-dom 中渲染過程以及對組件生命周期的處理進行詳細的梳理。在此之前,放張圖對本章的 Component 進行小結(jié)。

React.Component

渲染過程(react-dom

render 函數(shù)

在解析完了 React.ElementReact.Component 之后,可能很多人只是了解到了基礎結(jié)構(gòu)體的創(chuàng)建,還是感覺云里霧里。現(xiàn)在我們來理一理 react-dom 的整個渲染過程以及組件生命周期,從 constructor 組件的創(chuàng)建到 componentDidMount() 組件的掛載,最后再畫一個流程圖來進行總結(jié)。

react 本身只是一些基礎類的創(chuàng)建,比如 React.ElementReact.Component,而后續(xù)的流程則根據(jù)不同的平臺有不同的實現(xiàn)。我們這里以我們常用的瀏覽器環(huán)境為例,調(diào)用的是 ReactDOM.render() 方法(見下圖),我們現(xiàn)在就來對這個方法的渲染過程做一個詳細解析。

ReactDOM.render()

ReactDOM.render()

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

React

FiberNode

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

React

legacyRenderSubtreeIntoContainer 中的 第 28 行,就是 FiberNode 樹 的創(chuàng)建過程。

FiberNode 由內(nèi)部的 createFiber 函數(shù)進行創(chuàng)建(見下圖)。(這也是 React16 版本后作出的巨大更新,這個后面我們再展開說)。

React

FiberNode 被創(chuàng)建后掛載在了 FiberRoot.current 上。最后,App 組件作為根組件實例被返回,而接下來的渲染過程由 FiberNode 接管。

我們畫一個流程圖來幫助理解(見下圖)。

React

從上圖可以看出,我們的 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 渲染(見下圖)。

React
React

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)。
FiberNode 實例
Element 實例
  • 第 32 行:內(nèi)部開始遞歸調(diào)度,創(chuàng)建 FiberNode 樹。創(chuàng)建一個工作節(jié)點快照 workInProgress(初始值是根 FiberNode),圍繞著 workInProgressupdateQueue 展開構(gòu)建工作(見下圖);

根據(jù) updateQueue 更新節(jié)點(performUnitOfWork 將返回 workInProgress.child,直到所有節(jié)點遍歷完成)

更新過程

創(chuàng)建 FiberNode 子節(jié)點

進入 performUnitOfWork 函數(shù)內(nèi)部,我們省略掉一系列目前不需要關注的函數(shù),首先進入到 beginWork 函數(shù)(見下圖)。

beginWork

beginWork 函數(shù)會根據(jù) propscontext 是否改變(第 12~15 行)、當前當前節(jié)點優(yōu)先級是否高于正在更新的節(jié)點優(yōu)先級(第 17 行)這兩項來決定當前節(jié)點是否需要更新。

然后根據(jù)節(jié)點的標簽類型(tag),調(diào)用不同的函數(shù)進行內(nèi)部狀態(tài)更新。(見下圖)

beginWork

Root(FiberNode) 節(jié)點更新 - updateHostRoot

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

updateHostRoot
updateHostRoot

按照慣例,我們逐行解析函數(shù)所做的事情:

  • 第 2 行:將一系列有用的信息推入內(nèi)部棧(其中包括 #app 實例、context 信息等等)。
  • 第 5~7 行:收集節(jié)點新的 props 屬性和舊的 state、children 屬性。
  • 第 8 行:淺復制更新隊列,防止引用屬性互相影響;
  • 第 9 行:執(zhí)行更新隊列,主要的任務是將 React.Element 添加到 FibermemoizedStateupdateQueue 更新隊列中(見下圖);
React
  • 第 36~45 行:對上一步的 memoizedState 中的 element 進行進一步的處理,將其封裝成 FiberNode 然后掛載在 workInProgress(當前工作節(jié)點快照).child 屬性上,最后將該 child 返回。

到這一步,FiberNode 樹的第一個節(jié)點就已經(jīng)構(gòu)建完成并掛載,我們來畫一張流程圖進行梳理(下圖)。

React

App Component(FiberNode) 更新流程 - updateClassComponent

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

React

constructClassInstance

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

updateClassComponent
constructClassInstance

constructClassInstance 函數(shù)中(見上圖 1):

  • 第 96 行 創(chuàng)建 App Component 實例。
  • 第 101 行 將實例掛載在 workInProgressstateNode 屬性中(件上圖 2)
  • 第 107 行 最后返回該實例。

mountClassInstance

constructClassInstance 執(zhí)行完成后,接下來執(zhí)行第二個關鍵函數(shù) mountClassInstance。

constructClassInstance
React

mountClassInstance 函數(shù)中對 Component 實例進行掛載的一些初始化工作(見上圖)。我們從上圖可以看出,到了這里就開始了 Component 的生命周期鉤子邏輯。

在初始化實例的一些基礎屬性后,第 136~145 行執(zhí)行了 Component 的第一個生命周期鉤子,也就是 getDerivedStateFromProps(見上圖),它使用返回的對象來更新 state。

而緊隨其后(見下圖) 第 153 行 觸發(fā)了第二個生命周期鉤子 componentWillMount,主要用于在掛載前執(zhí)行一些操作。

React
React

finishClassComponent

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

React

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

React
React

React Element(FiberNode) 更新流程 - updateHostComponent

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

React
React

第 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 樹(見下圖)。

React

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

React
React

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

React

真實 DOM 樹構(gòu)建完成后,并且此時 workInProgress.child 也為 null,本次 workLoopSync 流程將在此結(jié)束,接下來進入到 finishSyncRender 函數(shù),進行節(jié)點的渲染工作。

渲染真實 DOM

react-dom 將在回調(diào)函數(shù)內(nèi)部將調(diào)用 insertOrAppendPlacementNodeIntoContainer 方法對 FiberNode 進行遍歷。(見下圖)

React

由上圖可知該函數(shù)會對 Host 節(jié)點(帶有 html tag 結(jié)構(gòu)的節(jié)點)調(diào)用 appendChildToContainer 函數(shù)進行渲染,其他節(jié)點取其 child 值進行遞歸調(diào)用。

appendChildToContainer 函數(shù)內(nèi)部,通過 appendChildFiberNode 上的 stateNode (我們在上一步創(chuàng)建好的 真實 DOM 樹)添加到 container(#app)中,然后調(diào)用 componentDidMount 生命周期鉤子函數(shù)。(見下圖)

React
React

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

React

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容