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

今天想了比較久的時間,準備開啓這一系列的文章,旨在對 React 系列的源碼進行深度解析,其中包含但不限於 react、react-dom、react-router... 等一系列優秀的 React 系列框架,最後再一一實現這些框架的簡易版本。

本篇文章將會是對 react 和 react-dom 渲染過程源碼的深度解析,我們將從官方 API 以及一些簡易 Demo 來進入 react 的內部世界,探討其中奧妙。

本文解析的 react 版本爲 v16.13.0,是我 fork 的官方倉庫,源碼地址

結構剖析

React

我們先從最基礎的結構開始解析,從上面這張圖來看看。我們創建了一個 App 類,繼承於 React.Component 類,在 render 生命週期函數中返回了一個 jsx 格式的 html 標籤集合。我們打開控制檯,查看創建的實例(如下圖):

React

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

字段 解釋
props Component 組件比作函數,props 就是函數的入參
context context 就是在組件樹之間共享的信息
refs 訪問原生 DOM 元素的集合
updater 負責 Component 組件狀態的更新
_reactInternalFiber App 實例對應的 FiberNode

一個 Component 實例的大致結構我們就解析完了,我們現在需要由內到外的繼續解析 Component 內部結構以及實現。

我們現在來看看 render 方法內部, 第 7 行 的內容屬於 jsx 語法,是一種 html 語法格式類似的高級模板語法。這一段我們需要借鑑一下官方的一張圖來進行解釋:

React

從上圖可以得知,jsx 語法都會被編譯成 React.createElement 函數,標籤屬性以及標籤內容都會編譯成對應的入參,由此可知我們所寫的 第 7 行 代碼在編譯後將會變成如下代碼:

React.createElement("section", {}, "Hello World");

React.createElement 所創建的對象就是 虛擬 DOM 樹,那麼內部創建的工作流程是什麼樣的?帶着這個問題,我們進入下一個章節。

React.createElement

我們剛纔得知 jsx 語法將會被編譯成 React.createElement 函數調用,而這個函數屬於 React 對象上的一個方法,現在我們就可以開始進入到源碼解析,查看內部實現。

React
React

上圖就是 React.createElement,我們先看最後返回的結果是 ReactElement 函數的執行結果,該函數最後返回的是一個 React Element 對象(後面會提到)。

所以 React.createElement 其實是一個工廠函數,用於創建 React Element 對象,我們再來看看這個工廠函數主要做了哪些工作。

  • 11-29 行:收集了 config 中的一些字段,並且將其他非內置字段添加到 props 對象中;
  • 31-40 行:將入參中的 children 參數掛載到 propschildren 字段中;(本示例中 "Hello World" 就是一個 “children”)
  • 42-49 行:收集組件(type 可能是字符串也有可能是 Component 實例,例如 <section /><App />)中設置的 defaultProps 屬性;

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

React

ReactElement 函數就比較一目瞭然了,返回了一個 element(React Element) 對象。React Element 對象其實就是一棵虛擬 DOM 樹($$typeof 字段表示了這是一個 React Element 類型),包含了標籤和屬性(attribute)信息。Component 執行 render 函數得到 虛擬 DOM 樹,再通過 react-dom 將其包裝成 FiberNode,然後被解析成 真實 DOM 樹 後渲染在頁面中(對應的容器內),這個我們後續再詳細解析,這裏就不展開了。

我們最後對 React Element 的創建過程畫一個流程圖來加深理解。(見下圖)

React

React.Component

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

React

Component 屬於一個構造函數(見上圖),Component 定義了幾個屬性,分別是 props、context、refs、updater,這些屬性在之前已經解釋過,這裏不再複述。這裏需要注意的是 Component 中的兩個方法 setStateforceUpdate,調用的都是內部 updater 的方法進行事件通知,將數據和 UI 更新的任務交給了內部的 updater 去處理,符合 單一職責設計原則

到這裏,Component 類的結構已經解析完成了。什麼,這就解析完成了?生命週期函數呢?渲染過程呢?一個都還沒有看到啊。彆着急,由於 React 內部的職責劃分與不同平臺實現,所以這部分根據不同平臺的實現被放在了 react-domreact-native 中。我們接下來就對我們常用的瀏覽器端,react-dom 中渲染過程以及對組件生命週期的處理進行詳細的梳理。在此之前,放張圖對本章的 Component 進行小結。

React.Component

渲染過程(react-dom

render 函數

在解析完了 React.ElementReact.Component 之後,可能很多人只是瞭解到了基礎結構體的創建,還是感覺雲裏霧裏。現在我們來理一理 react-dom 的整個渲染過程以及組件生命週期,從 constructor 組件的創建到 componentDidMount() 組件的掛載,最後再畫一個流程圖來進行總結。

react 本身只是一些基礎類的創建,比如 React.ElementReact.Component,而後續的流程則根據不同的平臺有不同的實現。我們這裏以我們常用的瀏覽器環境爲例,調用的是 ReactDOM.render() 方法(見下圖),我們現在就來對這個方法的渲染過程做一個詳細解析。

ReactDOM.render()
ReactDOM.render()

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

React

FiberNode

我們來看看 render 函數內部調用的 legacyRenderSubtreeIntoContainer 函數(見下圖)

React

legacyRenderSubtreeIntoContainer 中的 第 28 行,就是 FiberNode 樹 的創建過程。

FiberNode 由內部的 createFiber 函數進行創建(見下圖)。(這也是 React16 版本後作出的巨大更新,這個後面我們再展開說)。

React

FiberNode 被創建後掛載在了 FiberRoot.current 上。最後,App 組件作爲根組件實例被返回,而接下來的渲染過程由 FiberNode 接管。

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

React

從上圖可以看出,我們的 React Element 作爲 render 函數的入參,創建了一個 FiberNode 實例,也就是 FiberRoot.current,而後續的渲染過程都由這個根 FiberNode 接管,包括所有的生命週期。

遞歸構建 FiberNode 樹

在構建完了根 FiberNode 實例後,第 40 行 調用了 updaterContainer 函數開始構建整棵 FiberNode 樹以及完成 DOM 渲染(見下圖)。

React

React

updaterContainer 是一個比較關鍵的函數,我們來解析一下這個函數做了什麼:

  • 第 8~14 行React 內部的更新任務設置了優先級大小,優先級較高的更新任務將會中斷優先級較低的更新任務。React 設置了 ExpirationTime 任務過期時間,如果時間到期後任務仍未執行(一直被打斷),則會強制執行該更新任務。同時,React 內部也會將過期時間相近的更新任務合併成一個(批量)更新任務,從而達到批量更新減少消耗的效果。(React setState “異步” 更新原理
  • 第 16~21 行:從父組件中收集 context 屬性(由於這裏是 root 組件,所以父組件爲空)。
  • 第 23~31 行:構建更新隊列,第 24 行Element 實例(見下圖 1)掛載在 update 對象上,第 31 行 將更新隊列(updateQueue) 掛載在 FiberNode 實例(見下圖 2)。

FiberNode 實例

Element 實例

  • 第 32 行:內部開始遞歸調度,創建 FiberNode 樹。創建一個工作節點快照 workInProgress(初始值是根 FiberNode),圍繞着 workInProgressupdateQueue 展開構建工作(見下圖);

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

更新過程

創建 FiberNode 子節點

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

beginWork

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

然後根據節點的標籤類型(tag),調用不同的函數進行內部狀態更新。(見下圖)

beginWork

Root(FiberNode) 節點更新 - updateHostRoot

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

updateHostRoot

updateHostRoot

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

  • 第 2 行:將一系列有用的信息推入內部棧(其中包括 #app 實例、context 信息等等)。
  • 第 5~7 行:收集節點新的 props 屬性和舊的 state、children 屬性。
  • 第 8 行:淺複製更新隊列,防止引用屬性互相影響;
  • 第 9 行:執行更新隊列,主要的任務是將 React.Element 添加到 FibermemoizedStateupdateQueue 更新隊列中(見下圖);

React

  • 第 36~45 行:對上一步的 memoizedState 中的 element 進行進一步的處理,將其封裝成 FiberNode 然後掛載在 workInProgress(當前工作節點快照).child 屬性上,最後將該 child 返回。

到這一步,FiberNode 樹的第一個節點就已經構建完成並掛載,我們來畫一張流程圖進行梳理(下圖)。

React

App Component(FiberNode) 更新流程 - updateClassComponent

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

React

constructClassInstance

updateClassComponent 函數中,有三個關鍵函數,第一個就是 constructClassInstance

updateClassComponent

constructClassInstance

constructClassInstance 函數中(見上圖 1):

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

mountClassInstance

constructClassInstance 執行完成後,接下來執行第二個關鍵函數 mountClassInstance

constructClassInstance

React

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

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

而緊隨其後(見下圖) 第 153 行 觸發了第二個生命週期鉤子 componentWillMount,主要用於在掛載前執行一些操作。

React

React

finishClassComponent

在實例創建完成並且調用了上面兩個生命週期鉤子後,進入到最後一個關鍵函數 finishClassComponent

React

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

React

React

React Element(FiberNode) 更新流程 - updateHostComponent

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

React

React

第 13 行 會對組件的 children 類型進行判斷,判斷是否爲純文本內容,我們在此處就是純文本(section 標籤內的 Hello World 文本),隨後 nextChildren 就將被置空。

到這裏,nextChildren 已經爲空,完整的 FiberNode 樹就已經構建完成。beginWork 結束,接下來進入到新的流程。

創建 真實 DOM 樹

在結束了 beginWork 流程後,將調用 createInstance 函數創建 真實 DOM 樹(見下圖)。

React

createInstance 內部調用了 createElement 函數創建了 真實 DOM 節點(見下圖 1),然後通過遞歸遍歷 props 中的屬性(包括 children)構建了一棵 真實 DOM 樹(見下圖 2)

React

React

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

React

真實 DOM 樹構建完成後,並且此時 workInProgress.child 也爲 null,本次 workLoopSync 流程將在此結束,接下來進入到 finishSyncRender 函數,進行節點的渲染工作。

渲染真實 DOM

react-dom 將在回調函數內部將調用 insertOrAppendPlacementNodeIntoContainer 方法對 FiberNode 進行遍歷。(見下圖)

React

由上圖可知該函數會對 Host 節點(帶有 html tag 結構的節點)調用 appendChildToContainer 函數進行渲染,其他節點取其 child 值進行遞歸調用。

appendChildToContainer 函數內部,通過 appendChildFiberNode 上的 stateNode (我們在上一步創建好的 真實 DOM 樹)添加到 container(#app)中,然後調用 componentDidMount 生命週期鉤子函數。(見下圖)

React

React

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

React

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

React

在本篇文章主要是解析 React 的整個渲染過程,對 React 的整體結構和工作流程有了個初步的瞭解,下一章將圍繞着 Component 的生命週期與 setState 進行解析,如有需要可以關注專欄後續更新。

原文地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章