1 背景
React
在 18 版本引入了 Concurrent
模式,而这个模式则是用 Scheduler
这个包实现的。
在这篇文章里,我们来看下它的实现原理是什么。
2 前置知识
在正式阅读源码之前,我们还是有一些前置的知识需要了解的,分别是:
- 小顶堆:
Scheduler
内用来进行优先级排序的数据结构 - 浏览器事件循环机制:
Scheduler
实现的底层原理
2.1 小顶堆
堆是一棵完全二叉树,即除了最后一层外,所有层都完全填满,且最后一层的节点尽可能靠左排列。
最小堆:每个节点的值都小于或等于其子节点的值(父节点 ≤ 子节点)。
通常用数组实现,假设节点索引为 i:
- 左子节点:2i+1
- 右子节点:2i+2
- 父节点:(i - 1)/2
2.1.2 建堆
- 节点 push 到数组末尾
- 比较节点与父节点大小,如果比父节点小,那么节点往上浮直至到达堆顶,否则固定在后面的位置
2.1.3 出堆
- 将堆顶的节点替换为堆底的节点
- 堆顶节点跟左节点比较大小,若堆顶节点大则交换直至到达堆底
- 否则堆顶节点跟右节点比较大小,若堆顶节点大则交换直至到达堆底
- 若以上两个条件都不满足,那么保持目标节点在原位置不变
2.1.4 完整代码
type Heap = Array;
type Node = {
id: number,
sortIndex: number,
...
};
export function push(heap: Heap, node: T): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
export function peek(heap: Heap): T | null {
return heap.length === 0 ? null : heap[0];
}
export function pop(heap: Heap): T | null {
if (heap.length === 0) {
return null;
}
const first = heap[0];
const last = heap.pop();
if (last !== first) {
// $FlowFixMe[incompatible-type]
heap[0] = last;
// $FlowFixMe[incompatible-call]
siftDown(heap, last, 0);
}
return first;
}
function siftUp(heap: Heap, node: T, i: number): void {
let index = i;
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
function siftDown(heap: Heap, node: T, i: number): void {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (compare(left, node) < 0) {
if (rightIndex < length && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a: Node, b: Node) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
时间复杂度:
- 插入/删除最大(最小)值:O(logn)
- 建堆:O(n)
- 堆排序:O(nlogn)
2.1.5 总结
最后,我们对比一下其他排序的方式
数据结构 | 查找最小任务 | 插入任务 | 删除任务 | 适用性 |
---|---|---|---|---|
小顶堆(React 使用) | ✅ O(1) → 堆顶 | ✅ O(log n) | ✅ O(log n) | ✅ 高效 + 支持动态优先级调度 |
数组(遍历找最小) | ❌ O(n) | ✅ O(1) | ❌ O(n) | ❌ 查找最小任务慢 |
链表(按时间排序) | ✅ O(1)(头部是最小) | ❌ O(n) 插入需遍历 | ❌ O(n) 删除中间节点 | ❌ 插入慢,重排复杂 |
队列(FIFO) | ❌ 仅支持先进先出 | ✅ O(1) | ✅ O(1) | ❌ 无法排序,完全不适合 |
2.2 浏览器事件循环机制
事件循环的流程:
- 执行宏任务
- 执行同步任务
- 遇到宏任务就放到宏任务队列,遇到微任务就放到微任务队列
- 如果执行完同步任务,就清空微任务队列
- 从宏任务队列拿出一个宏任务执行,继续以上步骤
用图来看的话更加清晰一些:
如果我们站在浏览器渲染的角度上来看的话,流程是这样的:
2.2.1 requestIdleCallback
requestIdleCallback
可以在浏览器空闲的时间里去执行,文档上对它的描述如下:
为什么 React 不直接使用 requestIdleCallback
来实现 Scheduler
呢?主要有以下几个问题:
- 不确定性高:浏览器有可能根本没有空闲时间
- 兼容性问题:有些浏览器根本不支持
2.2.2 requestAnimationFrame
从上面的图可以知道,requestAnimationFrame
是在浏览器渲染前执行的,那么 React
为什么不用它来实现呢?主要原因是:
requestAnimationFrame
+setTimeout
的方法不可控requestAnimationFrame
在页面处于后台时会挂起,等页面激活了才会重新启动
2.2.3 微任务
我们知道微任务也参与了事件循环,那么为什么不用微任务呢?\
原因很简单,因为微任务有几个问题:
- 执行顺序不可控:微任务之间没有优先级关系,先来了就先执行
- 无法中断:比如说我在当前的微任务里不想执行了,想要到下一个事件循环里的微任务再执行是做不到的
- 可能会阻塞渲染
2.2.4 宏任务
宏任务的特点:在事件循环的最开始执行\
浏览器里常见的宏任务有:
setTimeout
setInterval
MessageChannel
为什么 React
里面不是用 setTimeout
把 task 放到宏任务里呢?原因是,当 setTimeout
嵌套调用被安排了 5 次之后,浏览器就会强制执行 4ms 的最小超时。
其实这个 4ms 的间隔是无法接受的,因为 React
设置的分片时间也就 5ms。
同样的,setInterval
也会有类似的问题。
2.2.4.1 MessageChannel
一方面 MessageChannel
使用的是消息通道机制,比 setTimeout
要更快并且基本没有延迟:
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
另外一方面,浏览器兼容性也是非常好,基本上都是支持的:
因此,最终 React
选择使用 MessageChannel
来实现 Scheduler
也就不奇怪了。
2.2.5 总结
调度方式 | 描述 | 优缺点 |
---|---|---|
requestIdleCallback | 浏览器空闲时执行的回调 | ❌ 不确定性高:可能根本不触发❌ 浏览器兼容性差(低端机、老浏览器) |
requestAnimationFrame | 渲染前调用,适合做视觉同步 | ❌ 只能每帧一次,受帧率限制❌ 页面隐藏时会挂起,调度中断❌ 与 setTimeout 组合不可控 |
微任务(Promise.then) | 每个宏任务后立即执行 | ❌ 无法中断:一旦开始就必须执行完❌ 无优先级控制❌ 会阻塞主线程、渲染卡顿 |
setTimeout / setInterval | 传统的异步任务调度方式(宏任务) | ❌ 最小延迟 ≥ 4ms(嵌套 5 层以上)❌ 精度不够,无法满足时间片(如 5ms)要求 |
MessageChannel ✅ | 现代浏览器提供的快速任务通道 | ✅ 几乎无延迟(比 setTimeout 快)✅ 支持跨标签页、后台运行✅ 可搭配 performance.now() 实现时间切片✅ 浏览器兼容性非常好 |
3 源码阅读
3.1 初始化
unstable_scheduleCallback
是一个入口函数,它的参数包含:
priorityLevel
:优先级callback
:回调任务options
- delay: 延迟
3.1.1 根据优先级分配超时时间
一开始就会根据 priorityLevel
来分配 timeout
,优先级越高 timeout
越小。换句话说,优先级越高就要越早执行,否则就会超时。
var currentTime = getCurrentTime();
var startTime;
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;
}
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// Times out immediately
timeout = -1;
break;
case UserBlockingPriority:
// Eventually times out
timeout = userBlockingPriorityTimeout;
break;
case IdlePriority:
// Never times out
timeout = maxSigned31BitInt;
break;
case LowPriority:
// Eventually times out
timeout = lowPriorityTimeout;
break;
case NormalPriority:
default:
// Eventually times out
timeout = normalPriorityTimeout;
break;
}
3.1.2 设置 task
后续这里会初始化一个 task,代码如下:
var expirationTime = startTime + timeout;
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
3.1.3 分配任务到对应的队列中
这一步是最后一步,也是最开始的一步:把 task 放到指定的队列中:
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
- 当 startTime > currentTime 时,
newTask
会被 push 到timerQueue
队列里,这时它的sortIndex
为startTime
。 - 当 startTime <= currentTime 时,
newTask
会被 push 到taskQueue
队列里,这时它的sortIndex
为expirationTime
看到这里我们可以认为:
- 即将开始的 task 会被放到
taskQueue
,而还没开始的 task 会被放到timerQueue
timerQueue
里的task
优先级是按照startTime
来标识的,越早开始优先级越高taskQueue
里的task
优先级是按照expirationTime
来标识的,越早过期优先级越高
而前面提到 expirationTime = startTime + timeout
,因而我们可以认为:\
即使有些 task 比较早开始,但是如果它本身的优先级不高,最终在 taskQueue
里的优先级也会被排在其他 task
后面。
3.2 timerQueue
前面提到,timerQueue
主要是用来存放一些延迟的任务,主要和下面这段代码有关:
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
}
当且仅当 taskQueue
队列为空且 newTask
在 timerQueue
队头里时会执行 requestHostTimeout
方法。
并且呢,在执行 requestHostTimeout
之前,还会判断下 isHostTimeoutScheduled
是否为 true
,如果为 true
就会调用 cancelHostTimeout
3.2.1 cancelHostTimeout
这个函数非常简单:
function cancelHostTimeout() {
// $FlowFixMe[not-a-function] nullable value
localClearTimeout(taskTimeoutID);
taskTimeoutID = ((-1: any): TimeoutID);
}
其中 localClearTimeout
其实就是 clearTimeout
:
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
3.2.2 requestHostTimeout
这个函数其实也是比较简单:
function requestHostTimeout(
callback: (currentTime: number) => void,
ms: number,
) {
// $FlowFixMe[not-a-function] nullable value
taskTimeoutID = localSetTimeout(() => {
callback(getCurrentTime());
}, ms);
}
就是一个普通的 setTimeout
函数封装的。
3.2.3 handleTimeout
看完上面的代码,我们在回到这句代码:
requestHostTimeout(handleTimeout, startTime - currentTime);
很容易可以明白,它的作用其实就是在延迟 startTime - currentTime
之后,执行一下 handleTimeout
方法。
function handleTimeout(currentTime: number) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback();
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
简单梳理下,它做了这些事情:
- 调用
advanceTimers
方法,并传入currentTime
- 如果
taskQueue
队列不为空,执行requestHostCallback
方法 - 否则,从
timerQueue
里取出一个timer
, 之后执行requestHostTimeout
方法
3.2.3.1 advanceTimers
照例先看代码:
function advanceTimers(currentTime: number) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// Timer was cancelled.
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// Timer fired. Transfer to the task queue.
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}
简单梳理下这里的逻辑:
- 从
timerQueue
里取出一个timer
- 如果
timer.callback
为空,那么就把它从timerQueue
里移除 如果
startTime <= currentTime
- 说明
timer
即将开始了,将它从timerQueue
里移除 - 设置
timer.sortIndex = timer.expirationTime
- 将它 push 到
taskQueue
里
- 说明
- 否则直接结束循环
- 如果循环没有结束,那么继续从
timerQueue
里取出一个timer
并重复以上步骤
总结一下 advanceTimers
的作用:
- 去掉无效的
timer
- 及时将到期的
timer
从timerQueue
放到taskQueue
里
3.2.3.2 requestHostCallback
这个函数不复杂:
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
里面调用的其实是 schedulePerformWorkUntilDeadline
这个方法:
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
// There's a few reasons for why we prefer setImmediate.
//
// Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
// (Even though this is a DOM fork of the Scheduler, you could get here
// with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
// https://github.com/facebook/react/issues/20756
//
// But also, it runs earlier which is the semantic we want.
// If other browsers ever implement it, it's better to use it.
// Although both of these would be inferior to native scheduling.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
// $FlowFixMe[not-a-function] nullable value
localSetTimeout(performWorkUntilDeadline, 0);
};
}
可以看到 schedulePerformWorkUntilDeadline
其实是做了一下兼容性处理:
- 如果当前
Nodejs
或者老 IE,那么就用localSetImmediate
方法 - 如果当前环境支持
MessageChannel
那么就直接用它 - 否则就退而求其次使用
setTimeout
方法
3.2.3.3 performWorkUntilDeadline
schedulePerformWorkUntilDeadline
方法里调的其实就是 performWorkUntilDeadline
:
const performWorkUntilDeadline = () => {
if (enableRequestPaint) {
needsPaint = false;
}
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
// remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
};
梳理一下逻辑:
- 调用
flushWork
并将返回值赋值给hasMoreWork
- 如果说
hasMoreWork
为true
,那么就继续调用schedulePerformWorkUntilDeadline
方法 - 否则就把
isMessageLoopRunning
标记为false
3.2.3.4 flushWork
flushWork
这段代码比较多,但是核心的其实只有两个地方,其他都是一些开关的逻辑:
function flushWork(initialTime: number) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskErrored(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod code path.
return workLoop(initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
首先就是这一段:
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
如果当前有定时器在跑的话,isHostTimeoutScheduled
直接标记成 false
并且调 cancelHostTimeout
函数来清除掉定时器。
其次就是这一段:
return workLoop(initialTime)
3.2.3.5 workLoop
这段代码非常多,可以先看一下:
function workLoop(initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (!enableAlwaysYieldScheduler) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
const callback = currentTask.callback;
if (typeof callback === 'function') {
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = null;
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentPriorityLevel = currentTask.priorityLevel;
// $FlowFixMe[incompatible-use] found when upgrading Flow
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskRun(currentTask, currentTime);
}
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = continuationCallback;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskYield(currentTask, currentTime);
}
advanceTimers(currentTime);
return true;
} else {
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskCompleted(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
if (enableAlwaysYieldScheduler) {
if (currentTask === null || currentTask.expirationTime > currentTime) {
// This currentTask hasn't expired we yield to the browser task.
break;
}
}
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
这里大概的逻辑是:
- 调用
advanceTimers
及时将到时的timer
放到taskQueue
里 - 从
taskQueue
里取出一个task
- 如果当前的分片时间 >= 5ms,那么就立刻退出循环并返回
false
- 如果
task.callback
不是一个function
就将它从taskQueue
里移除掉 - 如果
task.callback
是一个function
,那么就调用一下callback
方法并拿到continuationCallback
- 如果
continuationCallback
是一个function
,无论分片时间还剩余多少,这时候就要立刻将线程交还给主线程,此时会直接退出循环并返回true
,表示还有task
没有执行完 - 否则如果当前的
task
和taskQueue
堆顶的task
为同一个,就执行一下 pop 去掉堆顶的task
- 同时,再次调用一次
advanceTimers
,及时将到时的timer
放到taskQueue
里 - 重新从
taskQueue
里取出task
并重复以上步骤 - 循环结束后:
- 如果
currentTask !== null
则返回true
,表示还有task
没有执行完毕 - 如果
currentTask == null
,那么就从timerQueue
里取出一个timer
,并调用 `
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime),最终返回
false,表示已经执行完所有
task`
3.2.4 小结
上面提到了好几个关键的方法:
cancelHostTimeout
requestHostTimeout
handleTimeout
advanceTimers
requestHostCallback
performWorkUntilDeadline
flushWork
workLoop
在最后我们再来梳理一遍 timerQueue
整体的逻辑:
- 满足
startTime > currentTime
的task
会被 push 到timerQueue
里,之后会有一个定时器来执行handleTimeout
方法。如果在这之前已经有定时器在跑了,那就会把它取消掉。 handleTimeout
这里的逻辑,相当于是有一个轮询会一直从timerQueue
里找到到时的任务放到taskQueue
里,一旦taskQueue
不为空就会执行requestHostCallback
requestHostCallback
会调用schedulePerformWorkUntilDeadline
并发出一个宏任务,通常我们理解它会在下一个事件循环的开头里执行。在宏任务里的函数,会依次执行
performWorkUntilDeadline
flushWork
workLoop
- 最后在
workLoop
里会不断从taskQueue
里拿出task
来执行,执行之前如果发现分片时间 >= 5ms 就会立刻退出循环并返回true
。如果发现执行结果返回的是continuationCallback
也同样会马上退出循环并返回true
- 最后如果发现依然还有
task
,就会继续调用schedulePerformWorkUntilDeadline
并重复以上动作
3.3 taskQueue
入口这里的 taskQueue
逻辑主要是这些:
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
可以看出这里就是做两件事情:
- 将
task
放入taskQueue
- 调用
requestHostCallback
requestHostCallback
这个函数的逻辑已经在上面说过了,这里就不再赘述了。
4. 总结
在 React 18 引入的 Concurrent 模式中,调度系统的核心实现依赖于 Scheduler
包。通过小顶堆对任务进行优先级排序,再结合浏览器的事件循环机制,尤其是高效且兼容性好的 MessageChannel
,实现了可中断、可恢复的任务执行模型。本文从小顶堆的数据结构开始,逐步剖析了 Scheduler
的调度策略、延迟任务处理、任务执行分片以及整个调度链路中的关键方法与队列交互逻辑,帮助我们更清晰地理解 React 是如何「在合适的时间做合适的事情」的。