# 同步和异步入口判定
在 React 中,current
树与 workInProgress
树,两棵树可以对标 “双缓冲” 模式下的两套缓冲数据:当 current
树呈现在用户眼前时,所有的更新都会由 workInProgress
树来承接。workInProgress
树将会在用户看不到的地方(内存里)悄悄地完成所有改变,直到 current
指针指向它的时候,此时就意味着 commit 阶段已经执行完毕,workInProgress
树变成了那棵呈现在界面上的 current
树。
一般情况下,挂载就是一种特殊的更新,**ReactDOM.render 和 setState 一样,也是一种触发更新的姿势。**在 React 中,ReactDOM.render
、setState
、useState
等方法都是可以触发更新的,这些方法发起的调用链路很相似,是因为它们最后都会通过创建 update
对象来进入同一套更新工作流。
以下是更新链路:

在 dispatchAction
会完成 update
对象的创建:
function dispatchAction(fiber, queue, action){
// ...
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// ...
}
2
3
4
5
6
7
8
9
10
11
dispatchAction
中,调度的是当前触发更新的节点,这一点和挂载过程需要区分开来。在挂载过程中,updateContainer
会直接调度根节点。其实,对于更新这种场景来说,大部分的更新动作确实都不是由根节点触发的,而 render 阶段的起点则是根节点。因此在 scheduleUpdateOnFiber
中:
function scheduleUpdateOnFiber(fiber, lane, eventTime){
// ...
var root = markUpdateLaneFromFiberToRoot(fiber, lane);
}
2
3
4
markUpdateLaneFromFiberToRoot
将会从当前 Fiber 节点开始,向上遍历直至根节点,并将根节点返回。
在区分同步异步链路的地方有这么一个判断:
// schedueUpdateOnFiber
if(lane === SyncLane){
// ...
performSyncWorkOnRoot(root)
}else{
ensureRootIsScheduled(root, eventTime);
// ...
}
2
3
4
5
6
7
8
在同步的渲染链路中,lane === SyncLane
这个条件是成立的,因此会直接进入 performSyncWorkOnRoot
的逻辑,开启同步的 render 流程;而在异步渲染模式下,则将进入 else 的逻辑。
在 else 的逻辑里,ensureRootIsScheduled
方法决定了如何开启当前更新所对应的 render 阶段:
// ensureRootIsScheduled
if (newCallbackPriority === SyncLanePriority) {
// 同步更新的 render 入口
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 将当前任务的 lane 优先级转换为 scheduler 可理解的优先级
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
// 异步更新的 render 入口
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
2
3
4
5
6
7
8
9
10
performSyncWorkOnRoot
是同步更新模式下的 render 阶段入口,performConcurrentWorkOnRoot
是异步模式下的 render 阶段入口。它们对应的调度任务用到的函数分别是 scheduleSyncCallback
和 scheduleCallback
,这两个函数在内部都是通过调用 unstable_scheduleCallback
方法来执行任务调度的。而 unstable_scheduleCallback
正是 Scheduler(调度器)中导出的一个核心方法。
# 时间切片
以下第一张图就是未切片的效果,浏览器不可中断,很容易卡死,第二张图是 createRoot
切片后的效果(即开启 ConCurrent 模式),解决了 JS对主线程的超时占用问题。


再同步渲染中,循环创建 Fiber 节点,构建 Fiber 树的过程是由 workLoopSync
函数来触发的:
function workLoopSync(){
while(workInProgress !== null){
performUnitOfWork(workInProgress);
}
}
2
3
4
5
在 workLoopSync
中,只要 workInProgress
不为空,while 循环就不会结束,它所触发的是一个同步的 performUnitOfWork
循环调用过程。
而在异步渲染模式下,这个循环是由 workLoopConcurrent
来开启的。workLoopConcurrent
的工作内容和 workLoopSync
非常相似,仅仅在循环判断上有一处不同:
function workLoopConcurrent(){
while(workInProgress !== null && !shouldYield()){
performUnitOfWork(workInProgress);
}
}
2
3
4
5
当 shouldYield() 调用返回为 true 时,就说明当前需要对主线程进行让出了,此时 whille 循环的判断条件整体为 false,while 循环将不再继续。
在其他源码里有这两句:
var Scheduler_shouldYield = Scheduler.unstable_shouldYield,
// ......
var shouldYield = Scheduler_shouldYield;
2
3
shouldYield
的本体其实是 Scheduler.unstable_shouldYield
,也就是 Scheduler
包中导出的 unstable_shouldYield
方法,该方法本身比较简单:
{
exports.unstable_shouldYield = function(){
return exports.unstable_now() >= deadline;
}
}
2
3
4
5
其中 unstable_now
这里实际取的就是 performance.now()
的值,即 “当前时间”。那么 deadline 又是什么呢?它可以被理解为当前时间切片的到期时间,它的计算过程在 Scheduler
包中的 performWorkUntilDeadline
方法里可以找到:
var performWorkUntilDeadline = function(){
// ...
deadline = currentTime + yieldInterval;
// ...
}
// 在这行算式里,currentTime 是当前时间,yieldInterval 是时间切片的长度。注意,时间切片的长度并不是一个常量,它是由 React 根据浏览器的帧率大小计算所得出来的,与浏览器的性能有关。
2
3
4
5
6
7
做个小总结:React 会根据浏览器的帧率,计算出时间切片的大小,并结合当前时间计算出每一个切片的到期时间。在 workLoopConcurrent
中,while 循环每次执行前,会调用 shouldYield
函数来询问当前时间切片是否到期,若已到期,则结束循环、出让主线程的控制权。
# 优先级调度的实现
无论是 scheduleSyncCallback
还是 scheduleCallback
,最终都是通过调用 unstable_scheduleCallback
来发起调度的。unstable_scheduleCallback
是 Scheduler
导出的一个核心方法,它将 结合任务的优先级信息为其执行不同的调度逻辑。
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间
var currentTime = exports.unstable_now();
// 声明 startTime,startTime 是任务的预期开始时间
var startTime;
// 以下是对 options 入参的处理
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
// 若入参规定了延迟时间,则累加延迟时间
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
// timeout 是 expirationTime 的计算依据
var timeout;
// 根据 priorityLevel,确定 timeout 的值
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 优先级越高,timout 越小,expirationTime 越小
var expirationTime = startTime + timeout;
// 创建 task 对象
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
{
newTask.isQueued = false;
}
// 若当前时间小于开始时间,说明该任务可延时执行(未过期)
if (startTime > currentTime) {
// 将未过期任务推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中没有可执行的任务,而当前任务又是 timerQueue 中的第一个任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
......
// 那么就派发一个延时任务,这个延时任务用于检查当前任务是否过期
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 里处理的是当前时间大于 startTime 的情况,说明这个任务已过期
newTask.sortIndex = expirationTime;
// 过期的任务会被推入 taskQueue
push(taskQueue, newTask);
......
// 执行 taskQueue 中的任务
requestHostCallback(flushWork);
}
return newTask;
}
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
可以看出,unstable_scheduleCallback
的主要工作是针对当前任务创建一个 task,然后结合 startTime
信息将这个 task 推入 timerQueue
或 taskQueue
,最后根据 timerQueue
和 taskQueue
的情况,执行延时任务或即时任务。
- startTime:任务的开始时间。
- expirationTime:这是一个和优先级相关的值,expirationTime 越小,任务的优先级就越高。
- timerQueue:一个以 startTime 为排序依据的小顶堆,它存储的是 startTime 大于当前时间(也就是待执行)的任务。
- taskQueue:一个以 expirationTime 为排序依据的小顶堆,它存储的是 startTime 小于当前时间(也就是已过期)的任务。
小堆顶
堆是一种特殊的 完全二叉树 (opens new window)。如果对一棵完全二叉树来说,它每个结点的结点值都不大于其左右孩子的结点值,这样的完全二叉树就叫 “小顶堆 (opens new window)”。小顶堆自身特有的插入和删除逻辑,决定了无论我们怎么增删小顶堆的元素,其根节点一定是所有元素中值最小的一个节点。这样的性质,使得小顶堆经常被用于实现 优先队列。
// 以下为整个 unstable_scheduleCallback 方法中的核心逻辑
// 若当前时间小于开始时间,说明该任务可延时执行(未过期)
if (startTime > currentTime) {
// 将未过期任务推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中没有可执行的任务,而当前任务又是 timerQueue 中的第一个任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// ......
// 那么就派发一个延时任务,这个延时任务用于将过期的 task 加入 taskQueue 队列
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 里处理的是当前时间大于 startTime 的情况,说明这个任务已过期
newTask.sortIndex = expirationTime;
// 过期的任务会被推入 taskQueue
push(taskQueue, newTask);
// ......
// 执行 taskQueue 中的任务
requestHostCallback(flushWork);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
若判断当前任务是待执行任务,那么该任务会在 sortIndex
属性被赋值为 startTime
后,被推入 timerQueue。随后,会进入这样的一段判断逻辑:
// 若 taskQueue 中没有可执行的任务,而当前任务又是 timerQueue 中的第一个任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
......
// 那么就派发一个延时任务,这个延时任务用于将过期的 task 加入 taskQueue 队列
requestHostTimeout(handleTimeout, startTime - currentTime);
}
2
3
4
5
6
peek()
的入参是一个小顶堆,它将取出这个小顶堆的堆顶元素,taskQueue
里存储的是已过期的任务,peek(taskQueue)
取出的任务若为空,则说明 taskQueue
为空、当前并没有已过期任务。在没有已过期任务的情况下,会进一步判断 timerQueue
,也就是未过期任务队列里的情况。
小顶堆是一个相对有序的数据结构。timerQueue
作为一个小顶堆,它的排序依据其实正是 sortIndex 属性的大小。这里的 sortIndex
属性取值为 startTime
,意味着小顶堆的堆顶任务一定是整个 timerQueue 堆结构里 startTime 最小的任务,也就是需要最早被执行的未过期任务。
若当前任务(newTask)就是 timerQueue
中需要最早被执行的未过期任务,那么 unstable_scheduleCallback
会通过调用 requestHostTimeout
,为当前任务发起一个延时调用。
注意,这个延时调用(也就是 handleTimeout)并不会直接调度执行当前任务——它的作用是在当前任务到期后,将其从 timerQueue
中取出,加入 taskQueue
中,然后触发对 flushWork
的调用。真正的调度执行过程是在 flushWork
中进行的。flushWork 中将调用 workLoop,workLoop 会逐一执行 taskQueue 中的任务,直到调度过程被暂停(时间片用尽)或任务全部被清空。
// 以下是对过期任务的处理逻辑
{
// else 里处理的是当前时间大于 startTime 的情况,说明这个任务已过期
newTask.sortIndex = expirationTime;
// 过期的任务会被推入 taskQueue
push(taskQueue, newTask);
......
// 执行 taskQueue 中的任务
requestHostCallback(flushWork);
}
2
3
4
5
6
7
8
9
10
11
与 timerQueue
不同的是,taskQueue
是一个以 expirationTime
为 sortIndex
(排序依据)的小顶堆。对于已过期任务,React 在将其推入 taskQueue
后,会通过 requestHostCallback(flushWork)
发起一个针对 flushWork
的即时任务,而 flushWork
会执行 taskQueue
中过期的任务。
从 React 17.0.0 源码来看,当下 React 发起 Task 调度的姿势有两个:setTimeout、MessageChannel。在宿主环境不支持 MessageChannel 的情况下,会降级到 setTimeout。但不管是 setTimeout 还是 MessageChannel,它们发起的都是异步任务。
因此 requestHostCallback 发起的“即时任务”最早也要等到下一次事件循环才能够执行。“即时”仅仅意味它相对于“延时任务”来说,不需要等待指定的时间间隔,并不意味着同步调用。
以下是 unstable_scheduleCallback
的工作流:
