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
,里面放了准備處理的子節點。這里就做兩個動作:
- 處理更新隊列,得出新的state - processUpdateQueue方法
- 創建或者更新 FiberNode 的
child
,得到下一個工作循環的入參(也是FiberNode) - ChildReconciler方法
通過這兩個函數的詳細內容屬於比較通用的部分,將在后面單獨講解。
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. 改變 workInProgress/alternate/finishedWork 的身份
雖然是短短的一行代碼,但這個十分重要,所以單獨標記:
root.current = finishedWork;
這意味着,在 DomElement 副作用處理完畢之后,意味着之前講的緩沖樹
已經完成任務,翻身當主人,成為下次修改過程的current
。再來看一個全貌:
4. 提交裝載、變更后的生命周期調用操作
在這個流程中,也是遍歷 effect 鏈表,對於每種類型的節點,會做不同的處理。
4.1 ClassComponent
如果當前節點的 effectTag 有 Update 的標志位,則需要執行對應實例的生命周期方法。在初始化階段,由於當前的 Component 是第一次渲染,所以應該執行componentDidMount
,其他情況下應該執行componentDidUpdate
。
之前講到,updateQueue 里面也有 effect 鏈表。里面存放的就是之前各個 Update 的 callback,通常就來源於setState
的第二個參數,或者是ReactDom.render
的 callback
。在執行完上面的生命周期函數后,就開始遍歷這個 effect 鏈表,把 callback 都執行一次。
4.2 HostRoot
操作和 ClassComponent 處理的第二部分一致。
4.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中 雙緩沖
的兩棵樹 FiberNode Tree 角色互換,原來的 workInProgress 轉正- 執行對應 ReactComponent 的裝載后生命周期方法
componentDidMount
- 其他回調調用、autoFocus 處理
下一篇文章將會描述 React 的事件機制(但據說准備要重構),希望我不會斷耕。
寫完第一篇,React 版本已經到了 16.5.0 ……