React 源码漂流记:React 调和器核心源码解读(二)
# 目录
# 前言
在上一篇文章中,我们探讨了 updateContainer
、 scheduleUpdateOnFiber
和 ensureRootIsScheduled
三个核心函数的原理和作用。如果从整个渲染任务周期来看,主要涉及到生产首次渲染任务、任务在容器上的调度、任务基于调度器的分发几个过程。
细心的同学可能已经发现,在 ensureRootIsScheduled
中已经涉及到调度器的内容了,即 scheduleCallback
向调度器发起的调度请求。但是基于分层阅读的原则,本篇文章将不会讲解调度器的内容,我们只需要了解到异步任务是在调度器在合适的时间时回调执行的即可。这样,本文将继续调和器的解读,探讨同步任务和异步任务的调度和 Batch 阶段向 Render 阶段的过渡的过程。
在上一篇文章中对 ensureRootIsScheduled 的分析中我们了解到,ensureRootIsScheduled 对同步任务和异步任务分别进行了同步调度和异步调度的分发,分别调用 scheduleSyncCallback 和 scheduleCallback 这两个函数。现在我们就来具体分析这两个函数:
# scheduleSyncCallback
同步调度和异步调度相比有两个明显的不同之处:
- 同步调度一般不会经过调度器。
- 同步调度在调度器之外维护同步任务队列。
在下面的探讨中,我们会逐渐体会到这两点不同。
// src/react/packages/react-reconciler/src/ReactFiberSyncTaskQueue.new.js
export function scheduleSyncCallback(callback: SchedulerCallback) {
// 如果任务队列未初始化则初始化队列,将当前的任务加入同步任务队列
if (syncQueue === null) {
syncQueue = [callback];
} else {
syncQueue.push(callback);
}
}
2
3
4
5
6
7
8
9
scheduleSyncCallback 维护同步任务队列,在微任务的回调中执行 flushSyncCallbacks,此函数将全数消费同步任务队列。
# flushSyncCallbacks
此函数的主要作用是消费同步任务队列。
// src/react/packages/react-reconciler/src/ReactFiberSyncTaskQueue.new.js
export function flushSyncCallbacks() {
// isFlushingSyncQueue 是 syncQueue 的互斥锁,消费 callbacks 是一个互斥操作
if (!isFlushingSyncQueue && syncQueue !== null) {
// 关闭互斥锁
isFlushingSyncQueue = true;
let i = 0;
const previousUpdatePriority = getCurrentUpdatePriority();
try {
const isSync = true;
const queue = syncQueue;
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 中某个 Callback 发生了错误,则跳过此项
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
现在让我们回到上文所讲的同步任务相比于异步任务的区别,从宏观上上看,同步调度这样设计有如下的原因:
- 同步调度优先级最高,具有充足的原因绕过调度器使任务尽快的得到执行。至于使用微任务或者宏任务和间接达到调度的目的,是为了减小消费同步任务队列时产生的执行代码的压力。同时,同步任务队列在每次执行同步任务时将任务全数消费,也能够间接看出这一点。
- 同步调度具有较好的容错性,当某一个任务抛出了错误,程序会跳过错误的任务,并且在下一次
ImmediatePriority
优先级的异步调度中继续执行。
# scheduleCallback
这部分会与调度器交互,在 react 中,调度器是一个单独的模块,这里不再展开。现在需要知道的是,调度器会根据各种异步任务的优先级选择高优先级的任务进行回调,回调中执行 performSyncWorkOnRoot。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function scheduleCallback(priorityLevel, callback) {
// In production, always call Scheduler. This function will be stripped out.
return Scheduler_scheduleCallback(priorityLevel, callback);
}
2
3
4
5
其中, Scheduler_scheduleCallback
是调度器提供的方法。
# 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.');
}
// ......
// 获取 FiberRoot 上下一次执行的 lanes
let lanes = getNextLanes(root, NoLanes);
// 如果 lanes 中没有同步的 lanes
if (!includesSomeLane(lanes, SyncLane)) {
// There's no remaining sync work left.
ensureRootIsScheduled(root, now());
return null;
}
// 同步渲染 FiberRoot,并且返回渲染结果 exitStatus
let exitStatus = renderRootSync(root, lanes);
// 如果发生了普通错误,即 RootErrored,获取重试的优先级,并同步重试渲染 50 次
// ......
// 如果发生了严重错误,即 RootFatalErrored,抛出错误,将 FiberRoot 标记为 suspend
// ......
// We now have a consistent tree. Because this is a sync render, we
// will commit it even if something suspended.
// 渲染完毕,标记 finishedWork 和 finishedLanes,并且 Commit 当前的 FiberRoot
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
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
这个函数的主要作用如下:
- 调用 renderRootSync 在 FiberRoot 上进行渲染。根据返回的结果进行错误处理,根据错误的类型选择不同的错误处理策略。
- 标记 finishedWork 和 finishedLanes 用于下次调度。
- 调用
commitRoot
在 FiberRoot 上 Commit 此次渲染。 - 调用
ensureRootIsScheduled
确保 FiberRoot 上下一次被调度。
# performConcurrentWorkOnRoot
同步任务和异步任务的回调,无外乎是要进行两个最为重要的任务,一个是 Render
,一个是 Commit
。同步任务回调思路较为清晰,这两个过程也都是同步完成的。下面我们来看看异步任务回调会是怎样的情况。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot(root, didTimeout) {
// ......
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// ......
// Determine the next lanes to work on, using the fields stored
// on the root.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// ......
// 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.
// 在某些情况下并不会采用时间切片,如不包含阻塞或者过时的任务,转而采用同步渲染
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootIncomplete) {
// ......
const finishedWork: Fiber = (root.current.alternate: any);
// 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);
}
ensureRootIsScheduled(root, now());
}
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
总体过程与同步回调类似,不同的是这里调用了 finishConcurrentRender
来单独处理 Commit
的部分。
# 扩展
# 从 scheduleSyncCallback
看任务队列
同步任务队列是在调度器之外维护的一个简单的任务队列,下面我们来探讨一下这块的内容,可以为我们需要任务队列的场景做一些参考。
一个简单的任务队列包含两个部分的功能,一个是入队列(生产者),一个是消费队列(消费者)。这种生成和消费的思想,在队列中极为常见。生产者对应 scheduleSyncCallback
函数,消费者则对应 flushSyncCallbacks
函数。通过 OOP 的思想,我们可以据此完成下面的原型:
class Queue{
private queue = [];
private lock = true;
add(task) {
this.queue.push(task);
}
flush() {
if(!this.lock || !this.queue.length) return this;
this.lock = false;
const queue = this.queue;
let i = 0;
try {
// do tasks from the queue
for (; i < queue.length; i++) {
let task = queue[i];
do {
task = task();
} while (task !== null);
}
this.queue = [];
} catch (error) {
// process errors
} finally {
this.lock = 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
关于任务队列,这里有几个重点需要注意:
- 消费锁:防止任务池被两个消费者同时消费造成资源争抢。
- 使用任务快照:先获取任务快照再进行消费,消费期间生产的任务应该等到下次消费。
- 这里有一个问题,消费完毕之后直接把队列清空,可能造成消费期间生产的任务被丢失。
- 任务可以返回新任务,直到不在返回任务为止。
- 任务是同步执行的,任务被执行的顺序是可以保证的。
- 任务执行错误捕获和处理。
# 宽松的错误处理机制
react 本身是一个 UI 库,用于线上环境的视图的渲染必须要具有较为宽松的容错机制。其实我们可以从 Render
过程的错误处理来看出这一点。
以 renderRootSync
为例,上文中我们已知 renderRootSync
会返回 exitStatus
作为 Render 结果的执行状态。下面我们来探讨下这块的错误的处理机制(部分代码有简化)。
// 同步渲染 FiberRoot,并且返回渲染结果 exitStatus
let exitStatus = renderRootSync(root, lanes);
// 如果发生了普通错误,即 RootErrored
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.
// 获取重试的优先级,并同步重试渲染 50 次
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
}
}
// 如果发生了严重错误,即 RootFatalErrored,抛出错误,将 FiberRoot 标记为 suspend
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
// 清理执行栈
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root, now());
throw fatalError;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
处理机制总结如下:
- 普通错误:同步渲染重试最多 50 次,如果重试过程中遇到致命错误,由致命错误处理,否则放弃重试,并且提交 FiberTree。
- 致命错误:清理执行栈,将 FiberRoot 标记为 suspended,并且抛出这个错误。
可见只有遇到致命错误才会阻断程序执行。在后文中我们会详细探讨 exitStatus
的含义。
# 问题
# 如何理解 performSyncWorkOnRoot
和 performConcurrentWorkOnRoot
两个函数在调和器中的位置和作用?
- 从整体上看,这两个函数是作为同步任务或者异步任务的任务本身(内容)而存在的,也就是说,所谓调度的最终目标(也就是调度器的回调),就是要执行预设的任务。而这两个函数,正是任务本身,也是从
Batch
阶段向Render
阶段再向Commit
阶段过渡的入口。我们可能也注意到源码对于这两个函数的注释:
// This is the entry point for synchronous tasks that don't go
// through Scheduler
function performSyncWorkOnRoot() {}
// This is the entry point for every concurrent task, i.e. anything that
// goes through Scheduler.
function performConcurrentWorkOnRoot() {}
2
3
4
5
6
这里作为同步任务或者异步任务的 entry point
体现的正是如此。
- 从整体函数结构上看,这两个函数的结构高度的类似,这是因为此二者是针对同步任务和异步任务的后续处理过程的高度抽象。这是两个提纲挈领的、高度抽象的函数,而后续的同步渲染和异步任务也就此展开。
# 总结
通过本篇文章的探讨,有如下的重点内容需要关注:
- 调度是 React 中为达到
基于优先级调度任务
的目的的一个技巧,在 React 的调度中,有两处任务的调度,一处是用于处理同步任务的简单的任务队列,一处是用于处理异步任务的较为复杂的调度器。 - 调度的目的是执行任务,在执行的任务中,最重要的就是要完成后续的
Render
过程和Commit
过程。本文虽然只有四个函数,却分别对应了同步任务和异步任务的调度和回调任务的核心过程。对于核心函数的理解,要从函数在整体中所出的位置和所发挥的作用来理解,也要从核心的逻辑内容来理解。 performWork
是执行回调的任务,而这个过程也是从FiberRoot
上进行。可见,FiberRoot
对于FiberTree
的整体的结构至关重要。