揭秘React的执行机制:一文掌握并发模式与中断渲染
前置概念
Fiber Node: React 的每个组件都对应一个 Fiber 节点,它包含组件的类型、对应的 DOM 节点、子 Fiber、兄弟 Fiber、副作用、状态等信息。
Reconciliation: 协调过程是 React 用来比较新旧虚拟 DOM 树差异的过程,以确定哪些部分需要更新。
Work Loop: Fiber 架构引入了 work loop,它将渲染工作分解为小块任务,这些任务可以被暂停、中断和重新启动,这就是所谓的“时间分片”。
Scheduler: 调度器是 Fiber 架构的核心,它决定哪些任务现在应该执行,哪些可以延迟。
宏观理解
首先从宏观上理解react 的工作流程,主要分为首次渲染和更新两个部分,下面会结合部分源码,源码部分庞大复杂,这里为方便理解整体流程,仅给出核心部分源码,仅作参考。当然你也完全可以忽视代码部分,并不会影响你理解核心概念。
初始渲染(Initial Render)
1. 创建React根节点
当你调用ReactDOM.createRoot(container)
时,React会创建一个根节点。这个根节点是整个应用的起点。
const root = ReactDOM.createRoot(document.getElementById('root'));
// 源码参考:packages/react-dom/src/client/ReactDOMRoot.js
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
// ...省略代码...
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
return new ReactDOMRoot(root);
}
2. 开始渲染任务
调用root.render(<App />)
时,React开始渲染任务。这个过程是通过一系列内部机制完成的,包括创建更新、调度任务、构建Fiber树、协调和最终提交更改到DOM。
// 开始渲染任务
root.render(<App />);
// 源码参考:packages/react-dom/src/client/ReactDOMRoot.js
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
children: ReactNodeList,
): void {
const root = this._internalRoot;
// ...省略代码...
updateContainer(children, root, null, null);
};
3. 创建更新
创建更新任务,开始调度更新
// 源码参考:/react-reconciler/src/ReactFiberReconciler.new.js
function updateContainer(element, container, parentComponent, callback) {
const update = createUpdate(eventTime, lane);
// ...省略代码...
callback = callback === undefined ? null : callback;
if (callback !== null) {
// ...省略代码...
update.callback = callback;
}
const root = enqueueUpdate(current, update, lane);
// ...省略代码...
scheduleUpdateOnFiber(fiber, lane, eventTime);
// ...省略代码...
}
4. 调度更新
scheduleUpdateOnFiber
函数主要作用就是标记更新和调度更新。
ensureRootIsScheduled
函数就是调度更新的具体实现。调度的主要目的是保证应用的响应性和性能,通过智能地调度工作,确保高优先级的更新能够快速完成,同时允许低优先级的工作在必要时被推迟。
scheduleCallback
就是调用Scheduler
的能力进行调度任务,调度的任务可以分为同步任务和异步任务两大类,对应performConcurrentWorkOnRoot
和performSyncWorkOnRoot
。
// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
function scheduleUpdateOnFiber(
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ...省略代码...
markRootUpdated(root, lane, eventTime);
// ...省略代码...
ensureRootIsScheduled(root, eventTime);
// ...省略代码...
}
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ...省略代码...
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
return;
}
// ...省略代码...
// Check if there's an existing task. We may be able to reuse it.
if (existingCallbackPriority === newCallbackPriority) {
return;
}
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// ...省略代码...
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
if (supportsMicrotasks) {
// ...省略代码...
scheduleMicrotask(...);
} else {
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
// ...省略代码...
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// ...省略代码...
}
5. 协调(Reconciliation)
可以看到执行任务的过程,无论是并发模式还是同步模式,都会执行一个workLoop
,workLoop
再去调用performUnitOfWork
。而两者的区别就在于,并发使用了!shouldYield()
进行调度控制,这也是Scheduler
提供的能力。
performUnitOfWork
首先调用beginWork
来开始处理工作单元,如果beginWork
返回null,则意味着这个工作单元已经完成了,它会调用completeUnitOfWork
。如果beginWork
返回了另一个工作单元,则将其设置为下一个要处理的工作单元。其本质就是树的一个深度优先遍历。
beginWork
和 completeUnitOfWork
是 React Fiber 协调过程中的两个主要阶段。通过递归处理,收集需要更新的 Fiber 到 effectList,这个列表会在后续的提交阶段commitRoot
用来实际应用这些副作用到 DOM 上。
// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
// 并发(异步)任务
function performConcurrentWorkOnRoot(root, didTimeout) {
// ...省略代码...
// 是否时间切片?
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
// ...省略代码...
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, lanes);
}
}
}
// 同步任务
function performSyncWorkOnRoot(root) {
// ...省略代码...
flushPassiveEffects();
// ...省略代码...
let exitStatus = renderRootSync(root, lanes);
// ...省略代码...
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
);
// ...省略代码...
}
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// ...省略代码...
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...省略代码...
}
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// ...省略代码...
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...省略代码...
return workInProgressRootExitStatus;
}
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
// ...省略代码...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ...省略代码...
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
6. 执行提交
提交阶段分为多个子阶段,包括“变更前”阶段、“变更”阶段和“布局”阶段。
在“变更前”阶段,会调用如 getSnapshotBeforeUpdate 这样的生命周期方法。
提交“变更”阶段负责将渲染阶段计算出的更改应用到 DOM 或宿主环境中。
“布局”阶段是在 DOM 变更后读取它的阶段,通常用于执行布局操作,也是类组件的生命周期方法在此触发的原因。
// 源码参考:/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root) {
// ...省略代码...
commitBeforeMutationEffects(root, finishedWork);
commitMutationEffects(root, renderPriorityLevel, finishedWork);
commitLayoutEffects(finishedWork, root, lanes);
// ...省略代码...
}
更新渲染(Updates)
1. 触发更新
更新可以通过setState
、hooks或新的props传入等方式来触发。
// 源码参考:/react-reconciler/src/ReactFiberClassComponent.new.js
// 源码参考:/react-reconciler/src/ReactFiberHooks.new.js
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// ...省略代码...
if (root !== null) {
// 可以理解为触发更新的核心就是触发 scheduleUpdateOnFiber
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
}
// ...省略代码...
}
触发更新之后,更新的机制最终还是调用 scheduleUpdateOnFiber
流程概览图
核心机制
如何实现中断
React 的中断和恢复机制是基于其 Fiber 架构实现的。Fiber 架构是 React 16 中引入的一种新的内部机制,它允许 React 工作在单个任务上,如果需要,可以将控制权交回给浏览器,然后再继续从中断点恢复工作。这种能力是通过将工作分解成许多小的任务单元来实现的,每个任务单元完成后,React 都可以决定是继续处理下一个任务单元,还是中断以处理更紧急的事情。
1、work Loop
React 的渲染引擎有一个循环,称为“work loop”,在这个循环中,React 会遍历 Fiber 树来执行渲染工作。这个循环是可以被中断的,这就是所谓的“时间分片”。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
在这个函数中,workInProgress
是当前正在处理的 Fiber 节点,performUnitOfWork
是处理单个 Fiber 节点的函数,shouldYield
是一个检查是否应该中断工作循环的函数,它可能会基于浏览器的帧率或者剩余时间来决定。
2、Lane 模型
Lane模型,简单理解就是一种用来标记跟踪不同优先级的更新。每个更新被赋予一个或多个lanes,这些lanes代表了更新的优先级和类型。React调度器使用这些lanes来决定哪些更新应该被优先处理。通过这种机制,React能够更智能地处理多重更新,优先执行更重要的更新。
3、performUnitOfWork
这个函数负责处理单个 Fiber 节点的工作,包括调用生命周期方法、钩子函数,以及创建和更新 DOM 节点。
performUnitOfWork
会进行“深度优先遍历”,首先beginWork
开始处理一个 Fiber 节点,如果这个节点有子节点,它会返回第一个子节点,如果没有则调用completeUnitOfWork
。completeUnitOfWork
会完成一个 Fiber 节点的工作和收集副作用(side effects),如果当前节点有兄弟节点,那么下一个工作单元将是兄弟节点;如果没有将回溯到父节点,继续完成其余的工作。.
function performUnitOfWork(unitOfWork: Fiber): void {
// ...省略代码...
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ...省略代码...
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
4、shouldYield
shouldYield
是调用的Scheduler
的能力,用于判断当前 React 任务是否应该暂停以让浏览器处理其他任务的函数。它主要基于时间消耗和浏览器事件的检测来做出决策。
- 帧率和剩余时间:通过计算自上次任务开始以来已经消耗的时间(timeElapsed)来决定是否应该让出控制权。这个时间与浏览器的帧时间(5ms)进行比较,以确定是否已经占用了足够的时间。
- 高优先级任务的处理:检查是否存在高优先级任务,如用户输入或者画面更新。如果检测到这些任务,React 将暂停当前工作以响应这些任务。
这样的设计是为了确保用户界面的流畅性和响应性,同时允许长时间运行的任务在不影响用户体验的情况下执行。
function shouldYield() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
return false;
}
if (enableIsInputPending) {
// ...省略代码
}
}
5、Scheduler
React 的调度器是负责任务调度的部分。调度器通过unstable_scheduleCallback
统一调度任务,根据任务的优先级赋予不同的到期时间,并且通过两个队列去维护同步任务和延时任务。
执行任务是调用的requestHostCallback
,在 DOM 和 Worder 环境下,react 使用 MessageChannel 来管理任务执行,其内部也有一个 work loop,通过从任务队列中取出任务,并根据任务的过期时间来判断是否执行任务的回调,没错,这里也会用到shouldYield
的能力。
function workLoop(hasTimeRemaining, initialTime) {
// ...省略代码
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
// ...省略代码
const continuationCallback = callback(didUserCallbackTimeout);
// ...省略代码
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// ...省略代码
}
总结
React 的中断机制允许它在执行更新时能够根据任务的优先级、剩余时间策略来智能地中断和恢复工作。这与Fiber 节点的构造、工作循环、优先级模型、以及调度器的协作完成的。Fiber 架构的引入让 React 有了足够的灵活性来实现这种并发处理机制,从而提高应用的响应性和性能。
如何实现恢复
在 React 的 Fiber 架构中,每个工作单元(即 Fiber)在执行过程中都可以被中断,并且可以在稍后的时间恢复执行。React 实现这一机制的关键在于它如何追踪工作的进度,并且能够在适当的时候将控制权交回给浏览器,然后在浏览器空闲时恢复工作。下面是一些关键的概念和源码片段,帮助理解 React 是如何恢复中断的工作的:
1、Work In Progress (WIP) Tree
React 会维护一个正在进行中的工作树(WIP Tree),它是当前渲染阶段的 Fiber 树的副本。当工作开始时,React 会创建这个 WIP Tree,并在工作过程中更新它。
2、Fiber 结构
每个 Fiber 节点都包含了足够的信息,使得 React 可以在任何时候中断和恢复工作。这些信息包括:
child
: 指向第一个子节点sibling
: 指向下一个兄弟节点return
: 指向父节点memoizedState
: 记录组件的状态updateQueue
: 记录组件的更新
3、中断时的保存状态
当 React 决定中断当前的工作时(前面说过的shouldYield
函数触发),它不需要做特别的保存操作,因为所有的工作状态都已经存储在 WIP Fiber 树中了。当前的工作单元(当前执行的 Fiber 节点)和它的进度可以从正在处理的 Fiber 节点中直接获得。
4、恢复工作
当 React 准备恢复工作时,它会从上次中断的地方开始。这是通过重新开始 work loop 实现的,并且 work loop 会从上次中断的 workInProgress Fiber 节点开始执行。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
在上述循环中,如果 workInProgress
不为 null
,则意味着有工作需要继续进行。React 会调用 performUnitOfWork
来继续处理下一个工作单元。也就回到了performUnitOfWork
和completeUnitOfWork
总结
React 的恢复机制依赖于每个 Fiber 节点的结构和 work loop 的设计。每个 Fiber 节点都保存了足够的上下文信息,使得 React 可以在任何时候安全地中断和恢复工作。
如何理解 React 的调度器(scheduler)和 Fiber 架构中的循环(loop)
Scheduler 的循环(Scheduler Loop)
React 的调度器是负责管理任务的优先级和执行的部分。在 React 16+ 中,调度器利用了浏览器的 requestIdleCallback
(或者其自身的 polyfill)来实现一个循环,这个循环会在主线程空闲时执行低优先级的工作。
- 循环的目的:调度器的循环主要是为了分配和管理任务的执行。它会根据任务的优先级和过期时间来决定何时执行哪个任务。
- 工作方式:当 React 需要执行更新时,它会将更新任务注册到调度器中。调度器会根据当前的执行上下文(如浏览器是否空闲)和任务的优先级来决定执行的顺序和时间。
- 时间切片:调度器支持时间切片(time slicing),这意味着它可以将长任务切分成多个小任务,以避免阻塞主线程,从而提高应用的响应性。
Fiber 架构的循环(Fiber Loop)
Fiber 架构是 React 16 中引入的一种新的协调算法。它允许 React 在渲染过程中暂停、中断、恢复和重用工作。Fiber 架构中的“循环”并不是传统意义上的事件循环,而是 React 内部的一种递归遍历机制,是一个可以被中断和恢复的工作流程。
- 循环的目的:Fiber 架构的循环是为了在组件树上高效地执行协调操作,确定哪些组件需要更新、创建或销毁。
- 工作方式:Fiber 实际上是对组件树的节点的一个抽象,每个 Fiber 节点代表一个工作单元。React 会遍历这些节点,并根据需要执行相应的渲染工作。
- 中断和恢复:Fiber 架构的关键特性是能够将工作分割成小块,并在需要处理更高优先级的更新时中断当前工作。一旦高优先级的工作完成,它可以恢复之前的工作。
关联和区别
- 关联:调度器和 Fiber 架构共同协作以实现平滑的用户界面更新。调度器决定何时执行工作,而 Fiber 架构提供了一种机制来组织和管理这些工作。
- 区别:
-
- 层级不同:调度器工作在更高的层级,它管理所有的任务,不仅仅是与渲染相关的。而 Fiber 架构专注于渲染相关的任务。
- 工作单元不同:调度器的任务可能是任何类型的工作,比如数据获取或动画。Fiber 架构的工作单元是组件的渲染和协调。
- 目标不同:调度器的目标是优化整个应用的性能,特别是在多任务环境下。Fiber 的目标是优化渲染过程,使其更加平滑和可中断。
总结
总体来说,调度器和 Fiber 架构是互相补充的。调度器安排和优先处理任务,而 Fiber 架构则为这些任务的执行提供了灵活性和中断能力。通过这两个机制的结合,React 能够提供一个既快速又流畅的用户界面构建体验。
最后
本文在讨论React的渲染机制时省略了大量源码的细节,主要目的是为了理解其中的关键概念和核心流程。如果您发现有任何错误或不准确之处,欢迎提出指正,一起交流,非常感谢您的反馈和建议。
转载自:https://juejin.cn/post/7359833949168173082