How React Works (一)首次渲染
How React Works (一)首次渲染
一、前言
本文將會通過一個簡單的例子,結合React原始碼(v 16.4.2)來說明 React 是如何工作的,並且幫助讀者理解 ReactElement、Fiber 之間的關係,以及 Fiber 在各個流程的作用。看完這篇文章有助於幫助你更加容易地讀懂 React 原始碼。初期計劃有以下幾篇文章:
- 首次渲染
- 事件繫結
- 更新流程
- 排程機制
二、核心型別解析
在正式進入流程講解之前,先了解一下 React 原始碼內部的核心型別,有助於幫助我們更好地瞭解整個流程。為了讓大家更加容易理解,後續的描述只抽取核心部分,把 ref、context、非同步、排程、異常處理 之類的簡化掉了。
1. ReactElement
我們寫 React 元件的時候,通常會使用 JSX
來描述元件。 <p></p>
這種寫法經過babel轉換後,會變成以 React.createElement(type, props, children)形式。而我們的例子中, type
會是兩種型別: function
、 string
,實際上就是 App
的 constructor
方法,以及其他 HTML
標籤。
而這個方法,最終是會返回一個 ReactElement ,他是一個普通的 Object ,不是通過某個 class 例項化二來的,大概看看即可,核心成員如下:
key | type | desc |
---|---|---|
$$typeof | Symbol|Number | 物件型別標識,用於判斷當前Object是否一個某種型別的ReactElement |
type | Function|String|Symbol|Number|Object | 如果當前ReactElement是是一個ReactComponent,那這裡將是它對應的Constructor;而普通HTML標籤,一般都是String |
props | Object | ReactElement上的所有屬性,包含children這個特殊屬性 |
2. ReactRoot
當前放在ReactDom.js內部,可以理解為React渲染的入口。我們呼叫 ReactDom.render
之後,核心就是建立一個 ReactRoot ,然後呼叫 ReactRoot 例項的 render
方法,進入渲染流程的。
key | type | desc |
---|---|---|
render | Function | 渲染入口方法 |
_internalRoot | FiberRoot | 根據當前DomContainer建立的一個FiberTree的根 |
3. FiberRoot
FiberRoot 是一個 Object ,是後續初始化、更新的核心根物件。核心成員如下:
key | type | desc |
---|---|---|
current | (HostRoot)FiberNode | 指向當前已經完成的Fiber Tree 的Root |
containerInfo | DomContainer | 根據當前DomContainer建立的一個FiberTree的根 |
finishedWork | (HostRoot)FiberNode|null | 指向當前已經完成準備工作的Fiber Tree Root |
current、finishedWork,都是一個(HostRoot)FiberNode,到底是為什麼呢?先賣個關子,後面將會講解。
4. FiberNode
在 React 16之後,Fiber Reconciler 就作為 React 的預設排程器,核心資料結構就是由FiberNode組成的 Node Tree 。先參觀下他的核心成員:
key | type | desc |
---|---|---|
例項相關 | --- | --- |
tag | Number | FiberNode的型別,可以在packages/shared/ReactTypeOfWork.js中找到。當前文章 demo 可以看到ClassComponent、HostRoot、HostComponent、HostText這幾種 |
type | Function|String|Symbol|Number|Object | 和ReactElement表現一致 |
stateNode | FiberRoot|DomElement|ReactComponentInstance | FiberNode會通過stateNode繫結一些其他的物件,例如FiberNode對應的Dom、FiberRoot、ReactComponent例項 |
Fiber遍歷流程相關 | ||
return | FiberNode|null | 表示父級 FiberNode |
child | FiberNode|null | 表示第一個子 FiberNode |
sibling | FiberNode|null | 表示緊緊相鄰的下一個兄弟 FiberNode |
alternate | FiberNode|null | Fiber排程演算法採取了雙緩衝池演算法,FiberRoot底下的所有節點,都會在演算法過程中,嘗試建立自己的“映象”,後面將會繼續講解 |
資料相關 | ||
pendingProps | Object | 表示新的props |
memoizedProps | Object | 表示經過所有流程處理後的新props |
memoizedState | Object | 表示經過所有流程處理後的新state |
副作用描述相關 | ||
updateQueue | UpdateQueue | 更新佇列,佇列內放著即將要發生的變更狀態,詳細內容後面再講解 |
effectTag | Number | 16進位制的數字,可以理解為通過一個欄位標識n個動作,如Placement、Update、Deletion、Callback……所以原始碼中看到很多 &= |
firstEffect | FiberNode|null | 與副作用操作遍歷流程相關 當前節點下,第一個需要處理的副作用FiberNode的引用 |
nextEffect | FiberNode|null | 表示下一個將要處理的副作用FiberNode的引用 |
lastEffect | FiberNode|null | 表示最後一個將要處理的副作用FiberNode的引用 |
5. Update
在排程演算法執行過程中,會將需要進行變更的動作以一個Update資料來表示。同一個佇列中的Update,會通過next屬性串聯起來,實際上也就是一個單鏈表。
key | type | desc |
---|---|---|
tag | Number | 當前有0~3,分別是UpdateState、ReplaceState、ForceUpdate、CaptureUpdate |
payload | Function|Object | 表示這個更新對應的資料內容 |
callback | Function | 表示更新後的回撥函式,如果這個回撥有值,就會在UpdateQueue的副作用連結串列中掛在當前Update物件 |
next | Update | UpdateQueue中的Update之間通過next來串聯,表示下一個Update物件 |
6. UpdateQueue
在 FiberNode 節點中表示當前節點更新、更新的副作用(主要是Callback)的集合,下面的結構省略了CapturedUpdate部分
key | type | desc |
---|---|---|
baseState | Object | 表示更新前的基礎狀態 |
firstUpdate | Update | 第一個 Update 物件引用,總體是一條單鏈表 |
lastUpdate | Update | 最後一個 Update 物件引用 |
firstEffect | Update | 第一個包含副作用(Callback)的 Update 物件的引用 |
lastEffect | Update | 最後一個包含副作用(Callback)的 Update 物件的引用 |
三、程式碼樣例
本次流程說明,使用下面的原始碼進行分析
//index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root')); //App.js import React, { Component } from 'react'; import './App.css'; class App extends Component { constructor() { super(); this.state = { msg:'init', }; } render() { return ( <div className="App"> <p className="App-intro"> To get started, edit <code>{this.state.msg}</code> and save to reload. </p> <button onClick={() => { this.setState({msg: 'clicked'}); }}>hehe </button> </div> ); } } export default App;
四、渲染排程演算法 - 準備階段
從 ReactDom.render
方法開始,正式進入渲染的準備階段。
1. 初始化基本節點
建立 ReactRoot、FiberRoot、(HostRoot)FiberNode,建立他們與 DomContainer 的關係。

2. 初始化 (HostRoot)FiberNode
的 UpdateQueue
通過呼叫 ReactRoot.render
,然後進入 packages/react-reconciler/src/ReactFiberReconciler.js
的 updateContainer -> updateContainerAtExpirationTime -> scheduleRootUpdate
一系列方法呼叫,為這次初始化建立一個Update,把 <App />
這個 ReactElement 作為 Update 的 payload.element
的值,然後把 Update 放到 (HostRoot)FiberNode 的 updateQueue 中。
然後呼叫 scheduleWork -> performSyncWork -> performWork -> performWorkOnRoot
,期間主要是提取當前應該進行初始化的 (HostFiber)FiberNode,後續正式進入演算法執行階段。
五、渲染排程演算法 - 執行階段
由於本次是初始化,所以需要呼叫 packages/react-reconciler/src/ReactFiberScheduler.js
的 renderRoot
方法,生成一棵完整的FiberNode Tree finishedWork
。
1. 生成 (HostRoot)FiberNode 的 workInProgress
,即 current.alternate
。
在整個演算法過程中,主要做的事情是遍歷 FiberNode 節點。演算法中有兩個角色,一是表示當前節點原始形態的 current
節點,另一個是表示基於當前節點進行重新計算的 workInProgress/alternate
節點。兩個物件例項是獨立的,相互之前通過 alternate
屬性相互引用。物件的很多屬性都是 先複製再重建
的。
第一次建立結果示意圖:
這個做法的核心思想是 雙緩池技術(double buffering pooling technique)
,因為需要做 diff 的話,起碼是要有兩棵樹進行對比。通過這種方式,可以把樹的總體數量限制在 2
,節點、節點屬性都是延遲建立的,最大限度地避免記憶體使用量因演算法過程而不斷增長。後面的更新流程的文章裡,會了解到這個 雙緩衝
怎麼玩。
2. 工作執行迴圈
示意程式碼如下:
nextUnitOfWork = createWorkInProgress( nextRoot.current, null, nextRenderExpirationTime, ); .... while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }
剛剛建立的 FiberNode 被作為 nextUnitOfWork
,從此進入工作迴圈。從上面的程式碼可以看出,在是一個典型的遞迴的迴圈寫法。這樣寫成迴圈,一來就是和傳統的遞迴改迴圈寫法一樣,避免呼叫棧不斷堆疊以及呼叫棧溢位等問題;二來在結合其他 Scheduler
程式碼的輔助變數,可以實現遍歷隨時終止、隨時恢復的效果。
我們繼續深入 performUnitOfWork
函式,可以看到類似的程式碼框架:
const current = workInProgress.alternate; //... next = beginWork(current, workInProgress, nextRenderExpirationTime); //... if (next === null) { next = completeUnitOfWork(workInProgress); } //... return next;
從這裡可以看出,這裡對 workInProgress 節點進行一些處理,然後會通過 一定的遍歷規則
返回 next
,如果 next
不為空,就返回進入下一個 performUnitOfWork
,否則就進入 completeUnitOfWork
。
3. beginWork
每個工作的物件主要是處理 workInProgress
。這裡通過 workInProgress.tag
區分出當前 FiberNode 的型別,然後進行對應的更新處理。下面介紹我們例子裡面可以遇到的兩種處理比較複雜的 FiberNode 型別的處理過程,然後再單獨講解裡面比較重要的 processUpdateQueue
以及 reconcileChildren
過程。
3.1 HostRoot - updateHostRoot
HostRoot,即文中經常講到的 (HostRoot)FiberNode,表示它是一個 HostRoot 型別的 FiberNode ,程式碼中通過 FiberRoot.tag
表示。
前面講到,在最開始初始化的時候,(HostRoot)FiberNode 在初始化之後,初始化了他的 updateQueue
,裡面放了準備處理的子節點。這裡就做兩個動作:
child
通過這兩個函式的詳細內容屬於比較通用的部分,將在後面單獨講解。
3.2 ClassComponent - updateClassComponent
ClassComponent,即我們在寫 React 程式碼的時候自己寫的 Component,即例子中的 App
。
3.2.1 建立 ReactComponent
例項階段
對於尚未初始化的節點,這個方法主要是通過 FiberNode.type
這個 ReactComponent Constructor 來建立 ReactComponent 例項並建立與 FiberNode 的關係。
(ClassComponent)FiberNode 與 ReactComponent 的關係示意圖:
初始化後,會進入例項的 mount
過程,即把 Component render
之前的週期方法都呼叫完。期間, state
可能會被以下流程修改:
- 呼叫getDerivedStateFromProps
- 呼叫componentWillMount -- deprecated
- 處理因上面的流程產生的Update所呼叫的processUpdateQueue
3.2.2 完成階段 - 建立 child FiberNode
在上面初始化Component例項之後,通過呼叫例項的 render
獲取子 ReactElement,然後建立對應的所有子 FiberNode 。最終將 workInProgress.child
指向第一個子 FiberNode。
3.4 處理節點的更新佇列 - processUpdateQueue 方法
在解釋流程之前,先回顧一下updateQueue的資料結構:
從上面的結構可以看出,UpdateQueue 是存放整個 Update 單向連結串列的容器。裡面的 baseState 表示更新前的原始 State,而通過遍歷各個 Update 連結串列後,最終會得到一個新的 baseState。
對於單個 Update 的處理,主要是根據 Update.tag
來進行區分處理。
- ReplaceState:直接返回這裡的 payload。如果 payload 是函式,則使用它的返回值作為新的 State。
- CaptureUpdate:僅僅是將
workInProgress.effectTag
設定為清空ShouldCapture
標記位,增加DidCapture
標記位。 - UpdateState:如果payload是普通物件,則把他當做新 State。如果 payload 是函式,則把執行函式得到的返回值作為新 State。如果新 State 不為空,則與原來的 State 進行合併,返回一個
新物件
。 - ForceUpdate:僅僅是設定
hasForceUpdate
為 true,返回原始的 State。
整體而言,這個方法要做的事情,就是遍歷這個 UpdateQueue ,然後計算出最後的新 State,然後存到 workInProgress.memoizedState
中。
3.5 處理子FiberNode - reconcileChildren 方法
在 workInProgress 節點自身處理完成之後,會通過 props.children
或者 instance.render方法
獲取子 ReactElement。子 ReactElement 可能是 物件
、 陣列
、 字串
、 迭代器
,針對不同的型別進行處理。
- 下面通過 ClassComponent 及其
陣列型別 child
的場景來講解子 FiberNode 的建立、關聯流程(reconcileChildrenArray方法
):
在頁面初始化階段,由於沒有老節點的存在,流程上就略過了位置索引比對、兄弟元素清理等邏輯,所以這個流程相對簡單。
遍歷之前 render
方法生成的 ReactElement 陣列,一一對應地生成 FiberNode。FiberNode 有 returnFiber
屬性和 sibling
屬性,分別指向其父親 FiberNode和緊鄰的下一個兄弟 FiberNode。這個資料結構和後續的遍歷過程相關。
現在,生成的FiberNode Tree 結構如下:
圖中的兩個 (HostComponent)FiberNode
就是剛剛生成的子 FiberNode,即原始碼中的 <p>...</p>
與 <button>...</button>
。這個方法最後返回的,是第一個子 FiberNode,就通過這種方式建立了 (ClassComponent)FiberNode.child
與第一個子 FiberNode的關係。
這個時候,再搬出剛剛曾經看過的程式碼:
const current = workInProgress.alternate; //... next = beginWork(current, workInProgress, nextRenderExpirationTime); //... if (next === null) { next = completeUnitOfWork(workInProgress); } //... return next;
意味著剛剛返回的 child 會被當做 next
進入下一個工作迴圈。如此往復,會得到下面這樣的 FiberNode Tree :
生成這棵樹之後,被返回的是左下角的那個 (HostText)FiberNode。而重新進入 beginWork
方法後,由於這個 FiberNode 並沒有 child ,根據上面的程式碼邏輯,會進入 completeUnitOfWork
方法。
注意:雖然說本例子的 FiberNode Tree 最終形態是這樣子的,但實際上演算法是優先深度遍歷,到葉子節點之後再遍歷緊鄰的兄弟節點。如果兄弟節點有子節點,則會繼續擴充套件下去。
4. completeUnitOfWork
進入這個流程,表明 workInProgress 節點是一個葉子節點,或者它的子節點都已經處理完成了。現在開始要完成這個節點處理的剩餘工作。
4.1 建立DomElement,處理子DomElement 繫結關係
completeWork
方法中,會根據 workInProgress.tag
來區分出不同的動作,下面挑選2個比較重要的來進一步分析:
4.1.1 HostText
此前提到過, FiberNode.stateNode
可以用於存放 DomElement Instance。在初始化過程中,stateNode 為 null,所以會通過 document.createTextNode
建立一個 Text DomElement,節點內容就是 workInProgress.memoizedProps
。最後,通過 __reactInternalInstance$[randomKey]
屬性建立與自己的 FiberNode的聯絡。
4.1.2 HostComponent
在本例子中,處理完上面的 HostText 之後,排程演算法會尋找當前節點的 sibling 節點進行處理,所以進入了 HostComponent
的處理流程。
由於當前出於初始化流程,所以處理比較簡單,只是根據 FiberNode.tag
(當前值是 code
)來建立一個 DomElement,即通過 document.createElement
來建立節點。然後通過 __reactInternalInstance$[randomKey]
屬性建立與自己的 FiberNode的聯絡;通過 __reactEventHandlers$[randomKey]
來建立與 props 的聯絡。
完成 DomElement 自身的建立之後,如果有子節點,則會將子節點 append 到當前節點中。現在先略過這個步驟。
後續,通過 setInitialProperties
方法對 DomElement 的屬性進行初始化,而 <code>
節點的內容、樣式、 class
、事件 Handler等等也是這個時候存放進去的。
現在,整個 FiberNode Tree 如下:
經過多次迴圈處理,得出以下的 FiberNode Tree:
之後,回到紅色箭頭指向的 (HostComponent)FiberNode,可以分析一下之前省略掉的子節點處理流程。
在當前 DomElement 建立完畢後,進入 appendAllChildren
方法把子節點 append 到當前 DomElement 。由上面的流程可以知道,可以通過 workInProgress.child -> workInProgress.child.sibling -> workInProgress.child.sibling.sibling ....
找到所有子節點,而每個節點的 stateNode 就是對應的 DomElement,所以通過這種方式的遍歷,就可以把所有的 DomElement 掛載到 父 DomElement中。
最終,和 DomElement 相關的 FiberNode 都被處理完,得出下面的FiberNode 全貌:
4.2 將當前節點的 effect 掛在到 returnFiber 的 effect 末尾
在前面講解基礎資料結構的時候描述過,每個 FiberNode 上都有 firstEffect、lastEffect ,指向一個 Effect(副作用) FiberNode
連結串列。在處理完當前節點,即將返回父節點的時候,把當前的鏈條掛接到 returnFiber 上。最終,在 (HostRoot)FiberNode.firstEffect
上掛載著一條擁有當前 FiberNode Tree 所有副作用的 FiberNode 連結串列。
5. 執行階段結束
經歷完之前的所有流程,最終 (HostRoot)FiberNode 也被處理完成,就把 (HostRoot)FiberNode 返回,最終作為 finishedWork
返回到 performWorkOnRoot
,後續進入下一個階段。
六、渲染排程演算法 - 提交階段
所謂提交階段,就是實際執行一些周期函式、Dom 操作的階段。
這裡也是一個連結串列的遍歷,而遍歷的就是之前階段生成的 effect 連結串列。在遍歷之前,由於初始化的時候,由於 (HostRoot)FiberNode.effectTag
為 Callback
(初始化回撥)),會先將 finishedWork 放到連結串列尾部。結構如下:
每個部分提交完成之後,都會把遍歷節點重置到 finishedWork.firstEffect
。
1. 提交節點裝載( mount )前的操作
當前這個流程處理的只有屬於 ReactComponent 的 getSnapshotBeforeUpdate
方法。
2. 提交端原生節點( Host )的副作用(插入、修改、刪除)
遍歷到某個節點後,會根據節點的 effectTag 決定進行什麼操作,操作包括 插入( Placement )
、 修改( Update )
、 刪除( Deletion )
。
由於當前是首次渲染,所以會進入插入( Placement )流程,其餘流程將在後面的《How React Works(三)更新流程》中講解。
2.1 插入流程( Placement )
要做插入操作,必先找到兩個要素:父親 DomElement ,子 DomElement。
2.1.1 找到相對於當前 FiberNode 最近的父親 DomElement
通過 FiberNode.return
不斷往上找,找到最近的(HostComponent)FiberNode、(HostRoot)FiberNode、(HostPortal)FiberNode節點,然後通過 (HostComponent)FiberNode.stateNode
、 (HostRoot)FiberNode.stateNode.containerInfo
、 (HostPortal)FiberNode.stateNode.containerInfo
就可以獲取到對應的 DomElement 例項。
2.1.2 找到相對於當前 FiberNode 最近的所有 遊離子 DomElement
實際上,把目標是查詢當前 FiberNode底下所有鄰近的 (HostComponent)FiberNode、(HostText)FiberNode,然後通過 stateNode 屬性就可以獲取到待插入的 子DomElement 。
所謂 所有鄰近的
,可以通過這幅圖來理解:
圖中紅框部分 FiberNode.stateNode
,就是要被新增到父親 DomElement的 子 DomElement。
遍歷順序,和之前的生成 FiberNode Tree時順序大致相同:
a) 訪問child節點,直至找到 FiberNode.type
為 HostComponent 或者 HostRoot 的節點,獲取到對應的 stateNode ,append 到 父 DomElement中。
b) 尋找兄弟節點,如果有,就訪問兄弟節點,返回 a) 。
c) 如果沒有兄弟節點,則訪問 return 節點,如果 return 不是當前演算法入參的根節點,就返回a)。
d) 如果 return 到根節點,則退出。
3. 提交裝載、變更後的生命週期呼叫操作
在這個流程中,也是遍歷 effect 連結串列,對於每種型別的節點,會做不同的處理。
3.1 ClassComponent
如果當前節點的 effectTag 有 Update 的標誌位,則需要執行對應例項的生命週期方法。在初始化階段,由於當前的 Component 是第一次渲染,所以應該執行 componentDidMount
,其他情況下應該執行 componentDidUpdate
。
之前講到,updateQueue 裡面也有 effect 連結串列。裡面存放的就是之前各個 Update 的 callback,通常就來源於 setState
的第二個引數,或者是 ReactDom.render
的 callback
。在執行完上面的生命週期函式後,就開始遍歷這個 effect 連結串列,把 callback 都執行一次。
3.2 HostRoot
操作和 ClassComponent 處理的第二部分一致。
3.3 HostComponent
這部分主要是處理初次載入的 HostComponent 的獲取焦點問題,如果元件有 autoFocus
這個 props ,就會獲取焦點。
七、小結
本文主要講述了 ReactDom.render
的內部的工作流程,描述了 React 初次渲染的內在流程:
- 建立基礎物件: ReactRoot、FiberRoot、(HostRoot)FiberNode
- 建立 HostRoot 的映象,通過映象物件來做初始化
- 初始化過程,通過 ReactElement 引導 FiberNode Tree 的建立
- 父子 FiberNode 通過
child
、return
連線 - 兄弟 FiberNode 通過
sibling
連線 - FiberNode Tree 建立過程,深度優先,到底之後建立兄弟節點
- 一旦到達葉子節點,就開始建立 FiberNode 對應的 例項,例如對應的 DomElement 例項、ReactComponent 例項,並將例項通過
FiberNode.stateNode
建立關聯。 - 如果當前建立的是 ReactComponent 例項,則會呼叫呼叫
getDerivedStateFromProps
、componentWillMount
方法 - DomElement 建立之後,如果 FiberNode 子節點中有建立好的 DomElement,就馬上 append 到新建立的 DomElement 中
- 構建完成整個FiberNode Tree 後,對應的 DomElement Tree 也建立好了,後續進入提交過程
- 在建立 DomElement Tree 的過程中,同時會把當前的
副作用
不斷往上傳遞,在提交階段裡面,會找到這種標記,並把剛建立完的 DomElement Tree 裝載到容器 DomElement中 - 執行對應 ReactComponent 的裝載後生命週期方法
componentDidMount
下一篇文章將會描述 React 的事件機制(但據說準備要重構),希望我不會斷耕。
寫完第一篇,React 版本已經到了 16.5.0 ……