我們先來看一個簡單的 demo:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return (
<div className="container">
<div className="section">
<h1>This is the title.</h1>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</div>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
首次渲染的呼叫棧如下圖
以 performSyncWorkOnRoot 和 commitRoot 兩個方法為界限,可以把 ReactDOM.render 分為三個階段:
- Init
- Render
- Commit
Init Phase
render
很簡單,直接呼叫 legacyRenderSubtreeIntoContainer。
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
// 省略對 container 的校驗邏輯
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
這裡需要注意一點,此時的 element 已經不是 render 中傳入的
legacyRenderSubtreeIntoContainer
在這裡我們可以看到方法取名的重要性,一個好的方法名可以讓你一眼就看出這個方法的作用。legacyRenderSubtreeIntoContainer,顧名思義,這是一個遺留的方法,作用是渲染子樹並將其掛載到 container 上。再來看一下入參,children 和 container 分別是之前傳入 render 方法的 App 元素和 id 為 root 的 DOM 元素,所以可以看出這個方法會根據 App 元素生成對應的 DOM 樹,並將其掛在到 root 元素上。
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
// 省略對 callback 的處理邏輯
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 省略 else 邏輯
}
return getPublicRootInstance(fiberRoot);
}
下面來細看一下這個方法:
- 首次掛載時,會通過 legacyCreateRootFromDOMContainer 方法建立 container.reactRootContainer 物件並賦值給 root。 container 物件現在長這樣:
- 初始化 fiberRoot 為 root.internalRoot,型別為 FiberRootNode。fiberRoot 有一個極其重要的 current 屬性,型別為 FiberNode,而 FiberNode 為 Fiber 節點的對應的型別。所以說 current 物件是一個 Fiber 節點,不僅如此,它還是我們要構造的 Fiber 樹的頭節點,我們稱它為 rootFiber。到目前為止,我們可以得到下圖的指向關係:
- 將 fiberRoot 以及其它引數傳入 updateContainer 形成回撥函式,將回撥函式傳入 unbatchedUpdates 並呼叫。
unbatchedUpdates
主要邏輯就是呼叫回撥函式 fn,也就是之前傳入的 updateContainer。
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
const prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// fn 為之前傳入的 updateContainer
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
updateContainer
updateContainer 方法做的還是一些雜活,我們簡單總結一下:
- 計算當前 Fiber 節點的 lane(優先順序)。
- 根據 lane(優先順序),建立當前 Fiber 節點的 update 物件,並將其入隊。
- 排程當前 Fiber 節點(rootFiber)。
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const eventTime = requestEventTime();
// 計算當前節點的 lane(優先順序)
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 根據 lane(優先順序)計算當前節點的 update 物件
const update = createUpdate(eventTime, lane);
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 將 update 物件入隊
enqueueUpdate(current, update);
// 排程當前 Fiber節點(rootFiber)
scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}
scheduleUpdateOnFiber
接著會進入 scheduleUpdateOnFiber 方法,根據 lane(優先順序)等於 SyncLane,程式碼最終會執行 performSyncWorkOnRoot 方法。performSyncWorkOnRoot 翻譯過來,就是指執行根節點(rootFiber)的同步任務,所以 ReactDOM.render 的首次渲染其實是一個同步的過程。
到這裡大家可能會有個疑問,為什麼 ReactDOM.render 觸發的首次渲染是一個同步的過程呢?不是說在新的 Fiber 架構下,render 階段是一個可打斷的非同步過程。
我們先來看看 lane 是怎麼計算得到的,相關邏輯在 updateContainer 中的 requestUpdateLane 方法裡:
export function requestUpdateLane(fiber: Fiber): Lane {
const mode = fiber.mode;
if ((mode & BlockingMode) === NoMode) {
return (SyncLane: Lane);
} else if ((mode & ConcurrentMode) === NoMode) {
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
? (SyncLane: Lane)
: (SyncBatchedLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
return pickArbitraryLane(workInProgressRootRenderLanes);
}
// 省略非核心程式碼
}
可以看出 lane 的計算是由當前 Fiber 節點(rootFiber)的 mode 屬性決定的,這裡的 mode 屬性其實指的就是當前 Fiber 節點的渲染模式,而 rootFiber 的 mode 屬性其實最終是由 React 的啟動方式決定的。
React 其實有三種啟動模式:
- Legacy Mode:
ReactDOM.render(<App />, rootNode)
。這是目前 React App 使用的方式,當前沒有刪除這個模式的計劃,但是這個模式不支援一些新的功能。 - Blocking Mode:
ReactDOM.createBlockingRoot(rootNode).render(<App />)
。目前正在實驗中,作為遷移到 concurrent 模式的第一個步驟。 - Concurrent Mode:
ReactDOM.createRoot(rootNode).render(<App />)
。目前正在實驗中,在未來穩定之後,將作為 React 的預設啟動方式。此模式啟用所有新功能。
因此不同的渲染模式在掛載階段的差異,本質上來說並不是工作流的差異(其工作流涉及 初始化 → render → commit 這 3 個步驟),而是 mode 屬性的差異。mode 屬性決定著這個工作流是一氣呵成(同步)的,還是分片執行(非同步)的。
Render Phase
performSyncWorkOnRoot
核心是呼叫 renderRootSync 方法
renderRootSync
有兩個核心方法 prepareFreshStack 和 workLoopSync,下面來逐個分析。
prepareFreshStack
首先呼叫 prepareFreshStack 方法,prepareFreshStack 中有一個重要的方法 createWorkInProgress。
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 通過 current 建立 workInProgress
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 使 workInProgress 與 current 通過 alternate 相互指向
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 省略 else 邏輯
}
// 省略對 workInProgress 屬性的處理邏輯
return workInProgress;
}
下面我們來看一下 workInProgress 究竟是什麼?workInProgress 是 createFiber 的返回值,接著來看一下 createFiber。
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
return new FiberNode(tag, pendingProps, key, mode);
};
可以看出 createFiber 其實就是在建立一個 Fiber 節點。所以說 workInProgress 其實就是一個 Fiber 節點。
從 createWorkInProgress 中,我們還可以看出:
- workInProgress 節點是 current 節點(rootFiber)的一個副本。
- workInProgress 節點與 current 節點(rootFiber)通過 alternate 屬性相互指向。
所以到現在為止,我們的 Fiber 樹如下:
workLoopSync
接下來呼叫 workLoopSync 方法,程式碼很簡單,若 workInProgress 不為空,呼叫 performUnitOfWork 處理 workInProgress 節點。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
performUnitOfWork
performUnitOfWork 有兩個重要的方法 beginWork 和 completeUnitOfWork,在 Fiber 的構建過程中,我們只需重點關注 beginWork 這個方法。
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
目前我們只能看出,它會對當前的 workInProgress 節點進行處理,至於怎麼處理的,當我們解析完 beginWork 方法再來總結 performUnitOfWork 的作用。
beginWork
根據 workInProgress 節點的 tag 進行邏輯分發。tag 屬性代表的是當前 Fiber 節點的型別,常見的有下面幾種:
- FunctionComponent:函式元件(包括 Hooks)
- ClassComponent:類元件
- HostRoot:Fiber 樹根節點
- HostComponent:DOM 元素
- HostText:文字節點
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 省略非核心(針對樹構建)邏輯
switch (workInProgress.tag) {
// 省略部分 case 邏輯
// 函式元件(包括 Hooks)
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// 類元件
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// 根節點
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
// DOM 元素
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
// 文字節點
case HostText:
return updateHostText(current, workInProgress);
// 省略部分 case 邏輯
}
// 省略匹配不上的錯誤處理
}
當前的 workInProgress 節點為 rootFiber,tag 對應為 HostRoot,會呼叫 updateHostRoot 方法。
rootFiber 的 tag(HostRoot)是什麼來的?核心程式碼如下:
export function createHostRootFiber(tag: RootTag): Fiber {
// 省略非核心程式碼
return createFiber(HostRoot, null, null, mode);
}
在建立 rootFiber 節點的時候,直接指定了 tag 引數為 HostRoot。
updateHostRoot
updateHostRoot 的主要邏輯如下:
- 呼叫 reconcileChildren 方法建立 workInProgress.child。
- 返回 workInProgress.child。
function updateHostRoot(current, workInProgress, renderLanes) {
// 省略非核心邏輯
if (root.hydrate && enterHydrationState(workInProgress)) {
// 省略 if 成立的邏輯
} else {
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
resetHydrationState();
}
return workInProgress.child;
}
這裡有一點需要注意,通過檢視原始碼,你會發現不僅是 updateHostRoot 方法,所以的更新方法最終都會呼叫下面這個方法:
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
只是針對不同的節點型別,會有一些不同的處理,最終殊途同歸。
reconcileChildren
reconcileChildren 根據 current 是否為空進行邏輯分發。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
此時 current 節點不為空,會走 else 邏輯,呼叫 reconcileChildFibers 建立 workInProgress.child 物件。
reconcileChildFibers
根據 newChild 的型別進行不同的邏輯處理。
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 省略非核心程式碼
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// 省略其他 case 邏輯
}
}
// 省略非核心程式碼
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 省略非核心程式碼
}
newChild 很關鍵,我們先明確一下 newChild 究竟是什麼?通過層層向上尋找,你會在 updateHostRoot 方法中發現它其實是最開始傳入 render 方法的 App 元素,它在 updateHostRoot 中被叫做 nextChildren,到這裡我們可以做出這樣的猜想,rootFiber 的下一個是 App 節點,並且 App 節點是由 App 元素生成的,下面來看一下 newChild 的結構:
可以看出 newChild 型別為 object,$$typeof 屬性為 REACT_ELEMENT_TYPE,所以會呼叫:
placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
reconcileSingleElement
下面繼續看 reconcileSingleElement 這個方法:
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 省略 child 不存在的處理邏輯
if (element.type === REACT_FRAGMENT_TYPE) {
// 省略 if 成立的處理邏輯
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
方法的呼叫比較深,我們先明確一下入參,returnFiber 為 workInProgress 節點,element 其實就是傳入的 newChild,也就是 App 元素,所以這個方法的作用為:
- 呼叫 createFiberFromElement 方法根據 App 元素建立 App 節點。
- 將新生成的 App 節點的 return 屬性指向當前 workInProgress 節點(rootFiber)。此時 Fiber 樹如下圖:
- 返回 App 節點。
placeSingleChild
接下來呼叫 placeSingleChild:
function placeSingleChild(newFiber: Fiber): Fiber {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags = Placement;
}
return newFiber;
}
入參為之前建立的 App 節點,它的作用為:
- 當前的 App 節點打上一個 Placement 的 flags,表示新增這個節點。
- 返回 App 節點。
之後 App 節點會被一路返回到的 reconcileChildren 方法:
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
此時 workInProgress 節點的 child 屬性會指向 App 節點。此時 Fiber 樹為:
beginWork 小結
beginWork 的鏈路比較長,我們來梳理一下:
- 根據 workInProgress.tag 進行邏輯分發,呼叫形如 updateHostRoot、updateClassComponent 等更新方法。
- 所有的更新方法最終都會呼叫 reconcileChildren,reconcileChildren 根據 current 進行簡單的邏輯分發。
- 之後會呼叫 mountChildFibers/reconcileChildFibers 方法,它們的作用是根據 ReactElement 物件生成 Fiber 節點,並打上相應的 flags,表示這個節點是新增,刪除還是更新等等。
- 最終返回新建立的 Fiber 節點。
簡單來說就是建立新的 Fiber 位元組點,並將其掛載到 Fiber 樹上,最後返回新建立的子節點。
performUnitOfWork 小結
下面我們來小結一下 performUnitOfWork 這個方法,先來回顧一下 workLoopSync 方法。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
它會迴圈執行 performUnitOfWork,而 performUnitOfWork,我們已經知道它會通過 beginWork 建立新的 Fiber 節點。它還有另外一個作用,那就是把 workInProgress 更新為新建立的 Fiber 節點,相關邏輯如下:
// 省略非核心程式碼
// beginWork 返回新建立的 Fiber 節點並賦值給 next
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// 省略非核心程式碼
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
// 若 Fiber 節點不為空則將 workInProgress 更新為新建立的 Fiber 節點
workInProgress = next;
}
所以當 performUnitOfWork 執行完,當前的 workInProgress 都儲存著下次要處理的 Fiber 節點,為下一次的 workLoopSync 做準備。
performUnitOfWork 作用總結如下:
- 通過呼叫 beginWork 建立新的 Fiber 節點,並將其掛載到 Fiber 樹上
- 將 workInProgress 更新為新建立的 Fiber 節點。
App 節點的處理
rootFiber 節點處理完成之後,對應的 Fiber 樹如下:
接下來 performUnitOfWork 會開始處理 App 節點。App 節點的處理過程大致與 rootFiber 節點類似,就是呼叫 beginWork 建立新的子節點,也就是 className 為 container 的 div 節點,處理完成之後的 Fiber 樹如下:
這裡有一個很關鍵的地方需要大家注意。我們先回憶一下對 rootFiber 的處理,針對 rootFiber,我們已經知道在 updateHostRoot 中,它會提取出 nextChildren,也就是最初傳入 render 方法的 element。
那針對 App 節點,它是如何獲取 nextChildren 的呢?先來看下我們的 App 元件:
class App extends React.Component {
render() {
return (
<div className="container">
<div className="section">
<h1>This is the title.</h1>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</div>
</div>
);
}
}
我們的 App 是一個 class,React 首先會例項化會它:
之後會把生成的例項掛在到當前 workInProgress 節點,也就是 App 節點的 stateNode 屬性上:
然後在 updateClassComponent 方法中,會先初始化 instance 為 workInProgress.stateNode,之後呼叫 instance 的 render 方法並賦值給 nextChildren:
此時的 nextChildren 為下面 JSX 經過 React.createElement 轉化後的結果:
<div className="container">
<div className="section">
<h1>This is the title.</h1>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</div>
</div>
接著來看一下 nextChildren 長啥樣:
props.children 儲存的是其子節點,它可以是物件也可以是陣列。對於 App 節點和第一個 div 節點,它們都只有一個子節點。對於第二個 div 節點,它有三個子節點,分別是 h1、p、p,所以它的 children 為陣列。
並且 props 還會儲存在新生成的 Fiber 節點的 pendingProps 屬性上,相關邏輯如下:
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
return fiber;
}
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
// 省略非核心邏輯
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}
第一個 div 節點的處理
App 節點的 nextChildren 是通過構造例項並呼叫 App 元件內的 render 方法得到的,那對於第一個 div 節點,它的 nextChildren 是如何獲取的呢?
針對 div 節點,它的 tag 為 HostComponent,所以在 beginWork 中會呼叫 updateHostComponent 方法,可以看出 nextChildren 是從當前 workInProgress 節點的 pendingProps 上獲取的。
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// 省略非核心邏輯
const nextProps = workInProgress.pendingProps;
// 省略非核心邏輯
let nextChildren = nextProps.children;
// 省略非核心邏輯
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
我們之前說過,在建立新的 Fiber 節點時,我們會把下一個子節點元素儲存在 pendingProps 中。當下次呼叫更新方法(形如 updateHostComponent )時,我們就可以直接從 pendingProps 中獲取下一個子元素。
之後的邏輯同上,處理完第一個 div 節點後的 Fiber 樹如下圖:
第二個 div 節點的處理
我們先看一下第二個 div 節點:
<div className="section">
<h1>This is the title.</h1>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</div>
它比較特殊,有三個位元組點,對應的 nextChildren 為
下面我們來看看 React 是如何處理多節點的情況,首先我們還是會進入 reconcileChildFibers 這個方法:
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 省略非核心程式碼
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 省略非核心程式碼
}
newChild 即是 nextChildren,為陣列,會呼叫 reconcileChildrenArray 這個方法
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
// 省略非核心邏輯
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
// 省略非核心邏輯
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 省略非核心邏輯
}
下面來總結一下這個方法:
- 遍歷所有的子元素,通過 createChild 方法根據子元素建立子節點,並將每個字元素的 return 屬性指向父節點。
- 用 resultingFirstChild 來標識第一個子元素。
- 將子元素用 sibling 相連。
最後我們的 Fiber 樹就構建完成了,如下圖: