30 分钟看懂 React 框架原理
# 目录
# ReactDOMHostConfig
下面我们先探究一下生产更新的来源,主要分析 updateContainer 和 scheduleUpdateOnFiber 两个函数。
# updateContainer
更新是如何开始的?我们首先从 updateContainer 的调用来源开始追溯。
调用 updateContainer 的函数包括: legacyRenderSubtreeIntoContainer、ReactDOMRoot.prototype.render、ReactDOMRoot.prototype.unmount、hydrateRoot、scheduleRoot。ReactDOMRoot 由 ReactDOM.createRoot 创建。scheduleRoot 在 src/react/packages/react-reconciler/src/ReactFiberHotReloading.new.js
文件中。
// 应用层 API
legacyRenderSubtreeIntoContainer <- ReactDOM.hydrate
<- ReactDOM.render
<- ReactDOM.unmountComponentAtNode
2
3
4
由上面的分析可以看出,updateContainer 主要来源于应用层 API 的调用,这种调用生产了更新渲染的需求。那么 updateContainer 主要做了什么?
// src/react/packages/react-reconciler/src/ReactFiberReconciler.new.js
export function updateContainer(
// 待挂载的组件
element: ReactNodeList,
// 挂载容器
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// 获取 RootFiber
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
// 更新 container 的 context 信息
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 创建一个更新
const update = createUpdate(eventTime, lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}
// 将新建的更新入栈
enqueueUpdate(current, update, lane);
// 请求一次调度更新
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
updateContainer 关键功能如下:
- 继续完善 FiberRoot 信息:context、pendingContext。
- 初始化创建一个更新对象,添加属性 payload、callback,并且将更新加入更新队列。
- 调用 scheduleUpdateOnFiber,(向调度器) 发出一次调度的请求。
扩展
- createUpdate 创建更新在组件的生命周期或者用户行为中也会产生,如函数式组件(FC)中的 setState、useContext 的 dispatchAction、类组件的 this.setState 中,两种不同之处在于前者是应用层面的调用,后者则是组件层面的调用。
- 注意,scheduleUpdateOnFiber 的调度请求并不一定经过调度器,同步更新可能会跳过调度器的调度,后面会说明这一点。
# scheduleUpdateOnFiber
在 updateContainer 中,调用了 scheduleUpdateOnFiber 以在 fiber(此处指的是 RootFiber) 上调度一次更新,那么调度更新是如何在 fiber 上展开的呢?
首先来分析一下代码:
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// Lanes that were updated during the render phase (*not* an interleaved event).
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
// Whether to root completed, errored, suspended, etc.
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
export function scheduleUpdateOnFiber(
// RootFiber
fiber: Fiber,
// 调度优先级
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查嵌套更新,防止死循环
checkForNestedUpdates();
// 从 fiber 向上收集 lanes,root:FiberRoot = fiber.stateNode。对于 updateContainer 来说,这里 fiber 就是 RootFiber。
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// FiberRoot 不存在,说明 FiberTree 可能已经被废弃,不用更新
if (root === null) {
return null;
}
// Mark that the root has a pending update.
// 标记 root 即将更新,root.pendingLanes |= lane
markRootUpdated(root, lane, eventTime);
// 如果当前已经是 Render 阶段,且 root 是待处理的 HostRoot,这时跳过渲染的调度请求,并且追踪 lane,加入到 Render 阶段的 lanes,就在在当前调度的回调中参与渲染,或者等待下次渲染。
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// Track lanes that were updated during the render phase
// 收集当前的 lane 到 workInProgressRootRenderPhaseUpdatedLanes,表示在当前 render 中当前正在渲染的 RootFiber 上的优先级队列。
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
// 这里是正常的请求渲染调度的流程
} else {
if (root === workInProgressRoot) {
// 如果 workInProgressRootExitStatus 为 RootSuspendedWithDelay,则标记 root 为 suspend。这里是处理 suspended 组件。向 root 做标记以在后面的渲染中加以区分。
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
// 确保 HostRoot (向调度器)发起调度请求,
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// 如果是同步更新,context 还是 NoContext 阶段,fiber.mode 不是 ConcurrentMode,且 prd 环境 ReactCurrentActQueue.isBatchingLegacy 为 true
// 在初次加载时重置 workInProgressRootRenderTargetTime 并且 flushSyncCallbacks。
// 只在初始化应用时执行
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
return root;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
这个函数的核心功能如下:
- 从 fiber 向父级收集 lanes,并且计算出 HostRoot。
- 调用 ensureRootIsScheduled,确保 HostRoot 发起同步或者异步调度。
- 如果是初次启动应用,执行一些初始化工作。
下面我们将以 scheduleUpdateOnFiber 函数作为突破口,一层层的往下追溯,看看 React 的更新具体是怎样的流程,以及更新产生的来源到底是什么?
图注
<- 表示代码向下追溯,即前面的函数被后面的函数调用或者使用。<<< 表示省略追溯过程,因为从前面的过程中可以推出。... 表示忽略此过程。
scheduleUpdateOnFiber <- updateDehydratedSuspenseComponent <- updateSuspenseComponent <- attemptEarlyBailoutIfNoScheduledUpdate <- beginWork
<- beginWork
<- classComponentUpdater[enqueueSetState、enqueueReplaceState、enqueueForceUpdate] <- adoptClassInstance <- mountIndeterminateComponent <- beginWork
<- constructClassInstance <- updateClassComponent <- mountLazyComponent <- beginWork
<- beginWork
<- mountIncompleteClassComponent <- beginWork
<- callComponentWillMount <- mountClassInstance <- updateClassComponent <- mountLazyComponent <- beginWork
<- beginWork
<- mountIncompleteClassComponent <- beginWork
<- callComponentWillReceiveProps <- resumeMountClassInstance <- updateClassComponent <<< beginWork
<- updateClassInstance <<< beginWork
<- [DEV]forceStoreRerender <- updateStoreInstance <- mountSyncExternalStore ...
<- updateSyncExternalStore ...
<- subscribeToStore ...
<- [enableCache]refreshCache <- mountRefresh ...
<- dispatchReducerAction <- mountReducer <- reducer.dispatch[useReducer]
<- dispatchSetState <- useMutableSource <- stateHook.queue.dispatch <- dispatchAction[queue.reducer(state, dispatch)] <- useState
<- mountState <- HooksDispatcherOnMount.useState <- ReactCurrentDispatcher.current <- useState
<- mountTransition <- HooksDispatcherOnMount.useTransition <- useTransition
<- mountDeferredValue <- HooksDispatcherOnMount.useDeferredValue <- useDeferredValue
<- updateContainer <<< ReactDOM[hydrate、render、unmountComponentAtNode、createRoot、hydrateRoot、scheduleRoot]
<!-- <- attemptSynchronousHydration -->
<!-- <- attemptDiscreteHydration -->
<!-- <- attemptContinuousHydration -->
<!-- <- attemptHydrationAtCurrentPriority -->
beginWork <- workLoopConcurrent <- renderRootConcurrent <- performConcurrentWorkOnRoot <- ensureRootIsScheduled[root.callbackNode] <- scheduleUpdateOnFiber <<< beginWork
<- performUnitOfWork <- workLoopSync <- renderRootSync <- performConcurrentWorkOnRoot <<< beginWork
<- workLoopConcurrent <- renderRootConcurrent <- performConcurrentWorkOnRoot <<< beginWork
commitRoot <- finishConcurrentRender[RootErrored、RootSuspended、RootSuspendedWithDelay、RootCompleted] <- performConcurrentWorkOnRoot
<- performSyncWorkOnRoot
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
从上面的分析过程,我们可以得出很多重要的信息,总结如下:
# 一个闭环
从上面可以清晰的看出,从 scheduleUpdateOnFiber 往上追溯时,最终追溯到了 beginWork,然后从 beginWork 继续追溯,最终追溯到 scheduleUpdateOnFiber。大致过程如下:
钩子(生命周期)和事件系统 -> 应用层行为:dispatchSetState、classComponentUpdater、dispatchReducerAction、updateContainer
-> scheduleUpdateOnFiber -> ensureRootIsScheduled -> performSyncWorkOnRoot、performConcurrentWorkOnRoot
-> renderRootSync、renderRootConcurrent -> workLoopSync、workLoopConcurrent
-> performUnitOfWork -> beginWork -> mount、update 组件 -> 调用生命周期函数、响应事件系统 -> commitRoot -> ...
2
3
4
有以下几点需要注意:
- 应用层行为分别可以对应 FC setState、类组件 this.setState、useReducer 的 dispatch 和 ReactDOM 的一系列操作,如 hydrate、render、unmountComponentAtNode、createRoot、hydrateRoot、scheduleRoot。
- 从 ensureRootIsScheduled -> performConcurrentWorkOnRoot 中间还有调度器的调度过程,包括 scheduleSyncCallback 和 scheduleCallback,分别对应同步任务的调度和异步任务的调度。
- 在 performSyncWorkOnRoot、performConcurrentWorkOnRoot 执行之后,有一个 commitRoot 的操作。
- beginWork -> mount、update 组件 这个过程中涉及的内容较为繁琐,可以参考更新器的部分,本文不在赘述细节。
- commitRoot 在 performSyncWorkOnRoot(同步渲染)、或者 finishConcurrentRender(异步渲染)中调用。这并不是说在同步渲染中 commitRoot 和后面的 renderRootSync 是同时进行的,而是说 renderRootSync 是同步的,renderRootSync 执行完之后直接就执行了 commitRoot。从只有 finishConcurrentRender,而没有类似于 finishSyncRender 可以印证这一点。
一个完整的同步的渲染回调的调用函数栈,可以参考下图:
# 更新的来源
更新主要来源于应用层的一些行为:dispatchSetState、classComponentUpdater、dispatchReducerAction、updateContainer 和 ReactDOM 的一系列操作,如 hydrate、render、unmountComponentAtNode、createRoot、hydrateRoot、scheduleRoot。大致包括应用 mount 阶段对 ReactDOM API 调用引起的同步更新,类组件和 FC 状态更新、useReducer 的 dispatch 的调用,以及最新 ConcurrentAPI 如 useTransition、useDeferredValue 等。
下面我们从一个更为直接的角度来探究 React 更新的来源。现在我们知道,如果需要更新,则会将更新入栈,我们从这个角度来分析一下,在代码中搜索 enqueueUpdate(
:
调用 enqueueUpdate(
主要在如下几个模块:
- src/react/packages/react-reconciler/src/ReactFiberClassComponent.new.js
- src/react/packages/react-reconciler/src/ReactFiberHooks.new.js
- src/react/packages/react-reconciler/src/ReactFiberReconciler.new.js
- src/react/packages/react-reconciler/src/ReactFiberThrow.new.js
- src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
然后分析下分别在哪些函数中出现:
>> ReactFiberClassComponent
classComponentUpdater[enqueueSetState、enqueueReplaceState、enqueueForceUpdate]
>> ReactFiberHooks
dispatchReducerAction
dispatchSetState
>> ReactFiberReconciler
updateContainer
// 以下的 Update 为 ErrorUpdate
>> ReactFiberThrow
markSuspenseBoundaryShouldCapture <- throwException <- handleError <- renderRootSync/renderRootConcurrent
>> ReactFiberWorkLoop
captureCommitPhaseErrorOnRoot <- captureCommitPhaseError <- safelyCallCommitHookLayoutEffectListMount/safelyCallComponentWillUnmount/safelyCallComponentDidMount/safelyAttachRef/safelyDetachRef/safelyCallDestroy/commitBeforeMutationEffects_complete/commitMutationEffects_begin/commitMutationEffects_complete/commitLayoutMountEffects_complete/reappearLayoutEffects_complete/commitPassiveMountEffects_complete
captureCommitPhaseError <<< ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
可以看出,与前文的分析基本一致。
关于 useTransition、useDeferredValue 可以参考:淺談 React Concurrent Mode & 相關功能 (Fiber、Suspense、useTransition、useDeferredValue) (opens new window)
扩展
- scheduleUpdateOnFiber 一方面被 updateContainer 这样间接被外部应用层 API 调用,另一方面在组件层面由组件状态更新、context 更新、props 更新等行为在 react 内部调用(可以参考上文 scheduleUpdateOnFiber 调用来源)。
- checkForNestedUpdates 函数是为了防止行为更新的死循环,因为更新都要经过 scheduleUpdateOnFiber 函数提交调度更新,scheduleUpdateOnFiber 在 react 内部还是外部都会被频繁调用。checkForNestedUpdates 在内部维护变量 nestedUpdateCount 表示在当前的 commit 中 FiberRoot 更新的循环请求次数。
- 关于 react 中常见的位运算,可以参考 位运算初探 和 使用位运算提高枚举计算效率。
- 这里判断 root === workInProgressRoot 执行 suspended 组件的一些逻辑是因为,正常情况下这里 workInProgressRoot 应该为 null,只有当前的 FiberRoot 被标记为 workInProgressRoot 再回在这里特殊处理。正常情况下,workInProgressRoot 只有在 prepareFreshStack 函数被赋值为 root 的,而 prepareFreshStack 只有在 renderRootSync、renderRootConcurrent、pingSuspendedRoot 函数中正常执行或者在 performConcurrentWorkOnRoot、performSyncWorkOnRoot 中发生 FatalError 时执行。
# ensureRootIsScheduled
在上面对 scheduleUpdateOnFiber 的分析中,最重要的就是调用 ensureRootIsScheduled,以保证在 fiber 所在的 HostRoot 上调度更新,那么 HostRoot 上是如何继续调度的呢?
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
// 将饿死的 lanes 标记为超时以一并更新,超时的任务立即执行。
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
// 计算将要渲染的 lanes
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 无需要渲染的 lanes
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// We use the highest priority lane to represent the priority of the callback.
// 获取 lanes 中优先级最高的 lane 作为 callback 的优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
// 由于即将要生成新的 callback,先将现在的 callback 取消掉
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
// 如果是同步更新任务
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
// LegacyRoot 需要单独处理
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 请求同步调度回调 performSyncWorkOnRoot,将该回调加入同步回调队列
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// Flush the queue in a microtask.
// 支持微任务的浏览器不用再请求调度器的回调
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
if (executionContext === NoContext) {
// It's only safe to do this conditionally because we always
// check for pending work before we exit the task.
// 消费完同步回调队列
flushSyncCallbacks();
}
});
} else {
// Flush the queue in an Immediate task.
// 向调度器请求回调,优先级 ImmediatePriority(立即回调),回调后执行 flushSyncCallbacks 将同步回调队列消费完
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
// 同步更新执行完毕,将 newCallbackNode 置为 null,performSyncWorkOnRoot 不会用到此值
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
// 将 lanes 转化为事件优先级
switch (lanesToEventPriority(nextLanes)) {
// 离散事件优先级:ImmediateSchedulerPriority
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
// 连续事件优先级:UserBlockingSchedulerPriority
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
// 默认事件优先级:NormalSchedulerPriority
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
// Idle 事件优先级:IdleSchedulerPriority
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
// 向调度器请求相应优先级的异步回调,回调后执行 performConcurrentWorkOnRoot,Scheduler.scheduleCallback 返回调度的 callbackNode(newTask)
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新 callbackPriority 和 callbackNode 注意,此时异步回调并未执行
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
这个函数有以下几个关键作用:
- 更新 root 的 callbackNode、callbackPriority 属性。
- 同步更新调度:支持微任务的直接在微任务的回调执行 flushSyncCallbacks;调用 scheduleSyncCallback 将同步回调 performSyncWorkOnRoot 推入同步回调队列 syncQueue,并且以 ImmediateSchedulerPriority 的优先级向调度器请求同步回调,回调时执行 flushSyncCallbacks 消费同步队列中所有的同步回调。
- 异步更新调度:根据 nextLanes 计算事件优先级,并且转化为调度优先级,以相应的调度优先级向调度器发起异步回调,回调时执行 performConcurrentWorkOnRoot。
- 注意同步调度中调用了 scheduleSyncCallback、scheduleCallback 两个函数不可混淆,scheduleCallback 只是 Scheduler 提供的一种基于优先级机制的任务(回调)调度手段,performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 才是真正要通过调度执行的任务。同步的任务通过同步回调队列的方式进行了优化处理。scheduleSyncCallback 是将同步的任务加入同步任务队列。调度器不是不可替换的,如果浏览器支持微任务,同步任务的处理就可以交给微任务处理,而不经过调度器。
nextLanes 优先级是如何计算的?参见 Lane 与优先级
新知
- 调度更新都是通过 HostRoot 实现的,从 ensureRootIsScheduled 中之传入 root ,而没有传入 fiber 可见 HostRoot 对于 react 中整个调和过程的重要性。
# DiscreteEventPriority 和 ContinuousEventPriority
- 离散事件:discreteEvent,常见的如:click, keyup, change;
- 用户阻塞事件:userBlocking,常见的如:dragEnter, mouseMove, scroll;
- 连续事件:continuous,常见的如:error, progress, load;
更多解析可以参考:React 中的事件监听机制
# scheduleMicrotask 与 queueMicrotask
可以看到,如果浏览器支持 queueMicrotask,同步调度就不用经过调度器,而是直接交由微任务处理,这样既减少了 performSyncWorkOnRoot 执行的压力,同时又要比 setTimeout 这样的宏任务更快的执行。queueMicrotask 可以由 Promise 来模拟。queueMicrotask () 方法将微任务排队以调用 callback。
什么是 queueMicrotask?
queueMicrotask adds the function (task) into a queue and each function is executed one by one (FIFO) after the current task has completed its work and when there is no other code waiting to be run before control of the execution context is returned to the browser's event loop.
react 中 queueMicrotask 的使用:
const localPromise = typeof Promise === 'function' ? Promise : undefined;
export const supportsMicrotasks = true;
export const scheduleTimeout: any =
typeof setTimeout === 'function' ? setTimeout : (undefined: any);
export const scheduleMicrotask: any =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof localPromise !== 'undefined'
? callback =>
localPromise
.resolve(null)
.then(callback)
.catch(handleErrorInNextTick)
: scheduleTimeout; // TODO: Determine the best fallback here.
function handleErrorInNextTick(error) {
setTimeout(() => {
throw error;
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
了解更多关于微任务可以参考:
- 在 JavaScript 中通过 queueMicrotask () 使用微任务 (opens new window)
- An Introduction to JavaScript's queueMicrotask (opens new window)
- caniuse: queueMicrotask API (opens new window)
# scheduleSyncCallback 和 scheduleCallback
在上面对 ensureRootIsScheduled 的分析中我们知道,ensureRootIsScheduled 对同步任务和异步任务分别进行了同步调度和异步调度,分别调用 scheduleSyncCallback 和 scheduleCallback,那么具体同步调度和异步调度是如何进行的呢?
# scheduleSyncCallback 和 flushSyncCallbacks
scheduleSyncCallback 维护一个同步更新的任务队列,调用 flushSyncCallbacks 可全数消费任务任务中的所有任务。
// src/react/packages/react-reconciler/src/ReactFiberSyncTaskQueue.new.js
export function scheduleSyncCallback(callback: SchedulerCallback) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
if (syncQueue === null) {
syncQueue = [callback];
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
syncQueue.push(callback);
}
}
export function flushSyncCallbacks() {
// isFlushingSyncQueue 是 syncQueue 的互斥锁,消费 callbacks 是一个互斥操作
if (!isFlushingSyncQueue && syncQueue !== null) {
// Prevent re-entrance.
isFlushingSyncQueue = true;
let i = 0;
const previousUpdatePriority = getCurrentUpdatePriority();
try {
const isSync = true;
const queue = syncQueue;
// TODO: Is this necessary anymore? The only user code that runs in this
// queue is in the render or commit phases.
setCurrentUpdatePriority(DiscreteEventPriority);
// flush syncQueue,每个 callback 可以返回一个新的 callback
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
// 重置 syncQueue
syncQueue = null;
includesLegacySyncCallbacks = false;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
// 如果syncQueue 中每个 RootCallback 发生了错误,则跳过此项
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// Resume flushing in the next tick
// 调度在下一个同步调度中继续执行
scheduleCallback(ImmediatePriority, flushSyncCallbacks);
throw error;
} finally {
setCurrentUpdatePriority(previousUpdatePriority);
isFlushingSyncQueue = false;
}
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
这两个函数有如下关键作用:
- 维护同步回调的 syncQueue,以在同步回调到来时 flush syncQueue。
更多分析,请参考 React 首次渲染过程 中的 flushSyncCallbacks 函数分析。
# scheduleCallback
这部分会与调度器交互,在 react 中,调度器是一个单独的模块,这里不再展开。现在需要知道的是,调度器会根据各种异步任务的优先级选择高优先级的任务进行回调,回调中执行 performSyncWorkOnRoot。
调度器详细可以参考调度器章节内容。
# performSyncWorkOnRoot
从上面的分析中,我们已经知道了调度器同步调度的回调(可以不通过调度器)是由 performSyncWorkOnRoot 函数来处理的,下面我们来具体探究下这个函数:
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot(root) {
// 如果当前是 Render 阶段或者 Commit 阶段就报错,因为此时应该还在 Batch 阶段
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// 进入 Render 阶段前,先把 effects 和 callbacks 都消费掉。
flushPassiveEffects();
// 获取即将渲染的 lanes
let lanes = getNextLanes(root, NoLanes);
// 如果 lanes 中不包含 SyncLane,这说明没有同步的任务需要 renderRootSync,返回 ensureRootIsScheduled 重新调度一次
if (!includesSomeLane(lanes, SyncLane)) {
// There's no remaining sync work left.
ensureRootIsScheduled(root, now());
return null;
}
// 同步的 render HostRoot,返回 workInProgressRootExitStatus 表示退出时执行的状态,
// 包括 RootIncomplete、RootFatalErrored、RootErrored、RootSuspended、RootSuspendedWithDelay、RootCompleted
let exitStatus = renderRootSync(root, lanes);
// 如果发生普通错误,重试同步渲染,最多重试 50 次
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
// attempt, we'll give up and commit the resulting tree.
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
// 如果发生了致命错误,重置到非错误的状态,将 HostRoot 标记为 suspend 并且重新调度一次
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
// 将渲染后的 RootFiber 和 lanes 更新到 HostRoot 上
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 提交 HostRoot 上的更新,通知调度器在帧尾 yield 一次,浏览器重绘。
commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
// 保证每次 renderSync 之后,调度不会闲置
ensureRootIsScheduled(root, now());
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
分析一下这个函数:
- flushPassiveEffects 中主要调用 flushSyncCallbacks、commitPassiveUnmountEffects、commitPassiveMountEffects 这三个函数。调用这个函数的目的是什么?我们注意到, performSyncWorkOnRoot 会随着 scheduleSyncCallback 的调用而被执行,因此在 performSyncWorkOnRoot 执行时,很可能又有新的同步任务加入到同步回调队列中,所以为了提高同步渲染的效率,同时满足同步任务今早执行的目的,在 renderRootSync 之前,重新消费 SyncCallbacks。
- 调用 renderRootSync 渲染合同更新。返回一个退出状态,如果发生普通错误,采取重试策略;如果发生了致命错误,则采取重置策略。即使发生了普通的错误,本次 render 仍然被 commit,除非发生了 致命的错误,将抛出一个错误。
- commitRoot 需要调度器的支持,需要调度器在 raf 的帧的末尾提交浏览器绘制。
- performSyncWorkOnRoot 中任何情况的退出都要调用 ensureRootIsScheduled,保证调度不会被闲置。
- performSyncWorkOnRoot 可以不通过 Scheduler 的调度。
这个函数的关键作用如下:
- 调用 renderRootSync 同步渲染更新。
- 普通错误和致命错误的处理。
- 调用 commitRoot 提交 HostRoot 上的更新,触发浏览器 paint。
# performConcurrentWorkOnRoot
在上面的分析中,我们知道调度器异步任务调度的回调是由 performConcurrentWorkOnRoot 来处理的,下面我们具体来看下 performConcurrentWorkOnRoot 这个函数,并且与 performSyncWorkOnRoot 进行对比。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// If two updates are scheduled within the same event, we should treat their
// event times as simultaneous, even if the actual clock time has advanced
// between the first and second call.
let currentEventTime: number = NoTimestamp;
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
// Since we know we're in a React event, we can clear the current
// event time. The next update will compute a new event time.
currentEventTime = NoTimestamp;
currentEventTransitionLane = NoLanes;
// 如果当前是 Render 阶段或者 Commit 阶段就报错,因为当前应该还在 Batch 阶段
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// Flush any pending passive effects before deciding which lanes to work on,
// in case they schedule additional work.
const originalCallbackNode = root.callbackNode;
// 在 RenderConcurrent 之前再次消费完 callbacks 和 effects。
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// Something in the passive effect phase may have canceled the current task.
// Check if the task node for this root was changed.
// 检查 flushPassiveEffects 之后 callbackNode 是否改变,如果变化了,舍弃之后的渲染工作,因为新的调度已经来临
if (root.callbackNode !== originalCallbackNode) {
// The current task was canceled. Exit. We don't need to call
// `ensureRootIsScheduled` because the check above implies either that
// there's a new task, or that there's no remaining work on this root.
return null;
} else {
// Current task was not canceled. Continue.
}
}
// Determine the next lanes to work on, using the fields stored
// on the root.
// 计算当前要渲染的 lanes
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
// Defensive coding. This is never expected to happen.
return null;
}
// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// 判断是否要进行时间切片,这决定是改用同步 Render 还是继续异步 Render
// 包含有 Blocking 优先级的 lane 的任务,或者包含有过时 line 的任务要改为同步 Render
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
// 如果 Render 的结果不是 RootIncomplete,说明出现了异常情况
if (exitStatus !== RootIncomplete) {
if (exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
// 如果是普通错误,重试若干次
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
const renderWasConcurrent = !includesBlockingLane(root, lanes);
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes);
// We need to check again if something threw
if (exitStatus === RootErrored) {
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
}
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
}
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
}
// 完成 Render 之后,重新发起一次调度,以保证调度不会被中断
ensureRootIsScheduled(root, now());
// 如果新的新的调度节点将仍然发生在相同的 HostRoot,那就不用等调度器的调度,直接继续 performConcurrentWorkOnRoot
// 注意:ensureRootIsScheduled 会产生一个 newCallbackNode,更新 root.callbackNode
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
- currentEventTime 在 scheduleUpdateOnFiber 和 createUpdate 之前生成,即创建更新时生成,到 performConcurrentWorkOnRoot 被重置。每个 update 对象中有 eventTime。
- performConcurrentWorkOnRoot 必须通过 Scheduler 的调度才有机会执行。这与 performSyncWorkOnRoot 有所不同。
- 从整体来看,performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 流程是相似的,包括 executionContext 的判断、调用 renderRoot、错误判断等。
- 在 RenderConcurrent 之前再次消费完 callbacks 和 effects,原因是为了节省调度资源,同时保证更新被尽快的处理。当然,这里有一个细节,如果在 消费完 callbacks 和 effects 的过程中,root.callbackNode 发生了变化,则舍弃当前的 RenderRoot 等后续流程,因为新的调度即将来临。之所以舍弃当前的调度是因为,新的任务具有更高的优先级或者当前的调度已经没有任务可执行了。
- 在 RenderRoot 之前,仍然对 lanes 做了判断,如果 lanes 中有 blocking 优先级的任务或者过时的任务(过时的任务具有立即执行的优先级)就会使用同步渲染,没有上述的情况才会使用异步渲染。同步渲染和异步渲染分别调用 renderRootSync 和 renderRootConcurrent 执行。
- 在错误处理方面,对待普通的错误容忍度还是比较高的,recoverFromConcurrentError 将对错误执行之多 50 次的重试,注意,重试时是以 renderRootSync 即同步渲染执行的。
- 在 RenderRoot 之后,无论错误与否,都需要调用 ensureRootIsScheduled 以保证调度不会被闲置。由于异步渲染需要调度器调度的时间相对于同步渲染要长,这里做了一个优化处理,如果预知到新的调度仍然会出现在同一个 HostRoot 上,就可以直接继续调用自身以继续完成 HostRoot 上的渲染任务。
# finishConcurrentRender
在这个函数中,会根据 exitStatus 的状态执行不同的处理操作,主要是 commitRoot 操作。
function finishConcurrentRender(root, exitStatus, lanes) {
switch (exitStatus) {
// 这两种情况在 performConcurrentWorkOnRoot 中已经处理了,应该不会被执行到
case RootIncomplete:
case RootFatalErrored: {
throw new Error('Root did not complete. This is a bug in React.');
}
// RootErrored 的情况在 performConcurrentWorkOnRoot 中已经对错误情况重试了,这里将直接 commitRoot
case RootErrored: {
// We should have already attempted to retry this tree. If we reached
// this point, it errored again. Commit it.
commitRoot(root);
break;
}
// 如果 renderRoot 退出后返回 RootSuspended,则将当前的 HostRoot 标记为 suspended
case RootSuspended: {
markRootSuspended(root, lanes);
// We have an acceptable loading state. We need to figure out if we
// should immediately commit it or wait a bit.
if (
includesOnlyRetries(lanes) &&
// do not delay if we're inside an act() scope
!shouldForceFlushFallbacksInDEV()
) {
// This render only included retries, no updates. Throttle committing
// retries so that we don't show too many loading states too quickly.
const msUntilTimeout =
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
const nextLanes = getNextLanes(root, NoLanes);
if (nextLanes !== NoLanes) {
// There's additional work on this root.
break;
}
const suspendedLanes = root.suspendedLanes;
if (!isSubsetOfLanes(suspendedLanes, lanes)) {
// We should prefer to render the fallback of at the last
// suspended level. Ping the last suspended level to try
// rendering it again.
// FIXME: What if the suspended lanes are Idle? Should not restart.
const eventTime = requestEventTime();
markRootPinged(root, suspendedLanes, eventTime);
break;
}
// The render is suspended, it hasn't timed out, and there's no
// lower priority work to do. Instead of committing the fallback
// immediately, wait for more data to arrive.
// 在 root.timeoutHandle 挂载延时器,延迟执行 commitRoot
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
break;
}
}
// The work expired. Commit immediately.
commitRoot(root);
break;
}
case RootSuspendedWithDelay: {
markRootSuspended(root, lanes);
if (includesOnlyTransitions(lanes)) {
// This is a transition, so we should exit without committing a
// placeholder and without scheduling a timeout. Delay indefinitely
// until we receive more data.
break;
}
if (!shouldForceFlushFallbacksInDEV()) {
// This is not a transition, but we did trigger an avoided state.
// Schedule a placeholder to display after a short delay, using the Just
// Noticeable Difference.
// TODO: Is the JND optimization worth the added complexity? If this is
// the only reason we track the event time, then probably not.
// Consider removing.
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
const eventTimeMs = mostRecentEventTime;
const timeElapsedMs = now() - eventTimeMs;
const msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
// Don't bother with a very short suspense time.
if (msUntilTimeout > 10) {
// Instead of committing the fallback immediately, wait for more data
// to arrive.
root.timeoutHandle = scheduleTimeout(
commitRoot.bind(null, root),
msUntilTimeout,
);
break;
}
}
// Commit the placeholder.
commitRoot(root);
break;
}
// 如果渲染任务退出状态为 RootCompleted,直接 commitRoot
case RootCompleted: {
// The work completed. Ready to commit.
commitRoot(root);
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# renderRootSync
在上面的分析中,我们已经知道 performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 将会调用 renderRootSync 以完成同步渲染的任务,那么在 commitRoot 之前,同步渲染任务是如何完成的呢?
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// 先缓存下 executionContext,以便于错误恢复
const prevExecutionContext = executionContext;
// 将 RenderContext 加入 executionContext 中
executionContext |= RenderContext;
// 推出 ReactCurrentDispatcher.current,ReactCurrentDispatcher.current 将被重置为 ContextOnlyDispatcher
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
// 如果发现 workInProgressRoot 和 workInProgressRootRenderLanes 已经不要将要渲染的 root 和 lanes 了,将对 root 和 workInProgress 等变量执行清理和重置工作
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
prepareFreshStack(root, lanes);
}
// 开启一个 workLoopSync 循环,handleError 将捕获和处理 workLoopSync 执行中的错误。
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 当 workLoopSync 的循环退出后,所有渲染工作就执行完毕,
resetContextDependencies();
// 将 executionContext 恢复至渲染之前的状态,这表明 RenderContext 和 CommitContext(将在后文中加入) 将会被移除,之所以可以这样做,是因为我们刚好需要重置到 BatchedContext
executionContext = prevExecutionContext;
// 推入 ReactCurrentDispatcher.current,ReactCurrentDispatcher.current 将置为 prevDispatcher
popDispatcher(prevDispatcher);
// workLoopSync 循环执行完之后,workInProgress 应该置空,否则说明可能有工作没有执行完,所以报错
if (workInProgress !== null) {
// This is a sync render, so we should have finished the whole tree.
throw new Error(
'Cannot commit an incomplete root. This error is likely caused by a ' +
'bug in React. Please file an issue.',
);
}
// 继 workInProgress 置空之后,也重置 workInProgressRoot 和 workInProgressRootRenderLanes,这表明没有任何任务正在被渲染。
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// workInProgressRootExitStatus 是文件中的一个全局变量,表示 HostRoot 渲染之后退出时的状态,workLoopSync 会更新这个值。
return workInProgressRootExitStatus;
}
export function resetContextDependencies(): void {
// This is called right before React yields execution, to ensure `readContext`
// cannot be called outside the render phase.
currentlyRenderingFiber = null;
lastContextDependency = null;
lastFullyObservedContext = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
- 这里 executionContext 使用很巧妙,这里直接缓存了 executionContext 的值,并在执行完 workLoopSync 之后恢复。这是因为 renderRoot 中会在 executionContext 加入 RenderContext,而在 workLoop 中,则会在 executionContext 加入 CommitContext。因此这里直接将 executionContext 恢复至 BatchContext。
- 在执行 renderRoot 时,ReactCurrentDispatcher.current 将被重置为 ContextOnlyDispatcher,在执行完毕后恢复原值,这以为这什么呢?在 useState 和 useReducer 原理探析 一文中已经套就过 ReactCurrentDispatcher.current 中挂载的不同类型的 dispatcher 将会使用不同实现的 hooks。从下面的代码可以看出,在此阶段,所有的 hook 调用都会由 throwInvalidHookError 报错。
- 具体的渲染工作 workLoopSync 完成的,这是一个死循环,直到 workLoopSync 正常执行时退出。
- 渲染完毕后会执行一些重置工作,包括 workInProgress、workInProgressRoot、workInProgressRootRenderLanes 等。
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useInsertionEffect: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useId: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# renderRootConcurrent
在前面的分析中,我们知道 performConcurrentWorkOnRoot 函数会调用 renderRootConcurrent 去完成异步任务的渲染工作。现在我们来看下异步任务是如何完成的?这里与 renderRootSync 类似,将简略分析。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
resetRenderTimer();
prepareFreshStack(root, lanes);
}
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
popDispatcher(prevDispatcher);
executionContext = prevExecutionContext;
// Check if the tree has completed.
if (workInProgress !== null) {
return RootIncomplete;
} else {
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// Return the final exit status.
return workInProgressRootExitStatus;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- 这里调用了 workLoopConcurrent 以完成异步渲染任务,同样是死循环的设计,直到 workLoopConcurrent 执行成功。
- 不同的是,在 renderRootConcurrent 中,workLoopConcurrent 循环执行之后,workInProgress 是可以不为 null 的,在 中,workLoopSync 中将直接报错。需要注意的是,异步任务是可以不完成的,这时会返回 exitStatus 为 RootIncomplete。
# handleError
function handleError(root, thrownValue): void {
do {
let erroredWork = workInProgress;
try {
// Reset module-level state that was set during the render phase.
resetContextDependencies();
resetHooksAfterThrow();
// TODO: I found and added this missing line while investigating a
// separate issue. Write a regression test using string refs.
ReactCurrentOwner.current = null;
// 如果 workInProgress 不存在,或者 workInProgress.return 不存在则视为致命错误
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
// Set `workInProgress` to null. This represents advancing to the next
// sibling, or the parent if there are no siblings. But since the root
// has no siblings nor a parent, we set it to null. Usually this is
// handled by `completeUnitOfWork` or `unwindWork`, but since we're
// intentionally not calling those, we need set it here.
// TODO: Consider calling `unwindWork` to pop the contexts.
workInProgress = null;
return;
}
// 非致命错误将抛出异常、直接跳到完成渲染的回调
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
// Something in the return path also threw.
// 如果 workInProgress 上处理错误仍然抛出错误,找到 workInProgress.return 将其设置 erroredWork,错误将抛到父级处理。
thrownValue = yetAnotherThrownValue;
if (workInProgress === erroredWork && erroredWork !== null) {
// If this boundary has already errored, then we had trouble processing
// the error. Bubble it to the next boundary.
erroredWork = erroredWork.return;
workInProgress = erroredWork;
} else {
erroredWork = workInProgress;
}
continue;
}
// 只有没有 yetAnotherThrownValue 时才正常退出到 work loop
// Return to the normal work loop.
return;
} while (true);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
- 对于致命错误将直接将 workInProgress 置为 null,exitStatus 将抛出致命错误。
- 对于普通错误,抛出异常并且调用 completeUnitOfWork 执行渲染完毕的逻辑,这里 workInProgress 也会被值为 null。无论是致命错误还是普通错误都会正常跳出 workLoop。
- 对于处理过程中仍然抛出错误的错误,将向父级冒泡处理错误,知道错误被解决。
# workLoopSync
通过上面的分析可以知道,renderRootSync 会调用 workLoopSync 完成同步任务的渲染,现在我们来看下 workLoopSync 函数。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
10
- 这个函数就是在 workInProgress 还有值的时候执行 performUnitOfWork 消费 workInProgress。同步渲染会完成所有的任务,即处理 HostRoot 上所有 Fiber 上的同步更新。
- @noinline 是 chrome 浏览器的一种优化标注。 Google Closure Compiler 使用此标注是函数或者标注不被转换成 inline。详见:@noinline (opens new window)。浏览器 js 引擎通过 inline 优化提升代码解析效率,这通常对一些常量、不太执行的函数(如工厂函数,我们将在 vue 源码中看到)有效,对执行频率较高的函数和变量反而效率很低。这里 @noinline 标注意思是不要使用 inline 优化,以免降低代码执行效率。
提示
inline 优化是什么?
In computing, inline expansion, or inlining, is a manual or compiler optimization that replaces a function call site with the body of the called function. Inline expansion is similar to macro expansion, but occurs during compilation, without changing the source code (the text), while macro expansion occurs prior to compilation, and results in different text that is then processed by the compiler.
# workLoopConcurrent
在 renderRootConcurrent 函数中会调用 workLoopConcurrent 以完成异步渲染任务。workLoopConcurrent 与 workLoopSync 类似,不在赘述。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
- workInProgress 为 null 是会直接跳出,或者 shouldYield 时也会跳出。因此跳出 workLoopConcurrent 有两种情况,一是所有任务都执行完了,二是调度器即将产生 (yield) 新回调了。
# performUnitOfWork
从上面的分析中,我们已经知道无论是 workLoopSync 还是 workLoopConcurrent 最终都会调用 performUnitOfWork,这说明同步任务和异步任务的分发,最终都是通过 performUnitOfWork 来真正执行的。也就是说,对于执行渲染任务而言,performUnitOfWork 才是背后默默干活的工人。下面我们着重分析下这个函数。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
let next;
next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 在 fiber 与 Reconciliation 探析疑问中,我们分析过 Fiber 的结构,其中 fiber.alternate 是 fiber 上的一个版本池,当 fiber 更新以后会更新到 fiber.alternate 上。unitOfWork 本质是一个 fiber,其中 current 代表了 fiber 上一次更新的结果。
- beginWork 对当前的 workInProgress 进行 render,渲染后返回下一个需要更新的 fiber。在 beginWork 中,我们将具体探讨 fiber 的遍历的方法和结构。
- 当 next 为 null 时,所有 fiber render 完毕,因此执行 completeUnitOfWork。上一次执行 completeUnitOfWork 还是在 renderRootSync 和 renderRootConcurrent 的 handlerError 中遇到普通错误时。如果任务还没有完成,将 next 设置为 workInProgress,下次 performUnitOfWork 就会针对 next 进行 render。
# beginWork
在上面的分析中,我们已经知道 performUnitOfWork 会调用 beginWork 来执行 RootFiber 以及其下每一个 fiber 的渲染工作。那么具体执行了哪些工作呢?FiberTree 的遍历是如何进行呢?下面我们来看下 beginWork:
// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
// 上次渲染的 fiber
current: Fiber | null,
// 本次要渲染的 fiber
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 非初次渲染
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
// index 记录了当前 fiber 排在父列表中的下标
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
// 因为如下要真正渲染了,可以将优先级信息清空
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
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);
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
return updateForwardRef(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
// Resolve outer props first, then resolve inner props.
let resolvedProps = resolveDefaultProps(type, unresolvedProps);
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
const outerPropTypes = type.propTypes;
if (outerPropTypes) {
checkPropTypes(
outerPropTypes,
resolvedProps, // Resolved for outer only
'prop',
getComponentNameFromType(type),
);
}
}
}
resolvedProps = resolveDefaultProps(type.type, resolvedProps);
return updateMemoComponent(
current,
workInProgress,
type,
resolvedProps,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case IncompleteClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case OffscreenComponent: {
return updateOffscreenComponent(current, workInProgress, renderLanes);
}
case LegacyHiddenComponent: {
return updateLegacyHiddenComponent(current, workInProgress, renderLanes);
}
case CacheComponent: {
if (enableCache) {
return updateCacheComponent(current, workInProgress, renderLanes);
}
break;
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
- didReceiveUpdate 标记是否有属性或者 context 的更新,因为这两种情况会引发 re-render。
- 重要环境在于根据 workInProgress.tag 类别创建或者更新不同的组件,这里具体过程不在赘述,可以参考 workLoop 和 performUnitOfWork 探析一文。
- 需要注意的是,在上面 updateXXXComponent 的之后,会返回
workInProgress.child
,作为 performUnitOfWork 中的 next。根据这个线索,可以知道 react 中 performUnitOfWork 中是根据深度优先搜索(DFS)来进行遍历渲染的,那么这种遍历怎么扩展到兄弟节点呢?