深入 React 调度器:任务调度器的选择取舍
一帧中的任务
通常对于浏览器这一宿主环境来说,一帧中的任务会包含 事件循环 tick
(执行一个宏任务,然后清空所有已注册的微任务)、浏览器渲染周期 api 以及 浏览器渲染任务,整体执行流程如下:
- 执行一个宏任务 (Task): 从宏任务队列中选择一个最旧的、可运行的宏任务并执行它。这些宏任务通常包含
setTimeout
、setInterval
、MessageChannel
、I/O 操作
、UI 交互事件
等回调函数。 - 执行所有微任务 (Microtask Checkpoint): 执行当前宏任务执行完毕后存在的所有微任务。如果微任务执行过程中又添加了新的微任务,则继续执行,直到微任务队列为空。这些微任务通常包含
Promise.resolve().then(fn)
、queueMicrotask(fn)
、MutationObserver
等回调函数。 - 更新渲染 (Update the Rendering - 按需):
- 执行所有
requestAnimationFrame (rAF)
回调。 - 执行样式计算、布局、合成和绘制等渲染步骤 (如果浏览器确定需要渲染)。
- 执行所有
- 执行
requestIdleCallback
回调 (按需且有空闲时间): 如果当前帧有空闲时间,并且requestIdleCallback
队列中有回调,则执行它们。
例子说明
const channel = new MessageChannel();
channel.port1.onmessage = ({ data: { message } }) => {
console.log('MessageChannel Callback ', message);
};
const callback = () => {
requestIdleCallback(() => {
console.log('requestIdleCallback');
Promise.resolve().then(res => {
console.log('requestIdleCallback - Promise');
});
channel.port2.postMessage({ message: 'MessageChannel - 3' });
});
requestAnimationFrame(() => {
channel.port2.postMessage({ message: 'MessageChannel - 2' });
console.log('requestAnimationFrame');
Promise.resolve().then(res => {
console.log('requestAnimationFrame - Promise');
});
});
Promise.resolve().then(res => {
console.log('Promise');
channel.port2.postMessage({ message: 'MessageChannel - 1' });
});
};
callback();
上述例子的 输出结果 为:
Promise
requestAnimationFrame
requestAnimationFrame - Promise
MessageChannel Callback MessageChannel - 1
MessageChannel Callback MessageChannel - 2
requestIdleCallback
requestIdleCallback - Promise
MessageChannel Callback MessageChannel - 3
解释:
初始状态: 所有任务队列均为空。
Tick 1: 初始脚本执行
- 宏任务: 主线程开始执行全局脚本,这是第一个宏任务。
callback()
函数被调用。Promise.resolve().then(...)
注册一个 微任务 A。requestIdleCallback(...)
注册一个 Idle 宏任务 到 Idle 任务队列。requestAnimationFrame(...)
注册一个 rAF 宏任务 到 动画帧回调队列。
- 宏任务结束: 脚本同步代码执行完毕。
- 微任务阶段: 清空微任务队列。
- 执行 微任务 A 的回调,控制台输出
Promise
日志(✅)。 - 执行
channel.port2.postMessage({ message: 'MessageChannel - 1' })
,注册一个 宏任务 M1 到 标准任务队列。
- 执行 微任务 A 的回调,控制台输出
Tick 1
执行结束- 当前队列状态:
- rAF 队列:
[rAF 回调]
- 任务队列:
[M1]
- Idle 队列:
[Idle 回调]
- rAF 队列:
- 当前队列状态:
- 宏任务: 主线程开始执行全局脚本,这是第一个宏任务。
Tick 2: 动画帧回调
- 宏任务: 事件循环进入下一轮。根据优先级,浏览器准备渲染更新,因此从 rAF 队列 中取出 rAF 宏任务 执行。
channel.port2.postMessage({ message: 'MessageChannel - 2' })
被调用,注册一个宏任务 M2 到标准任务队列。- 控制台输出
requestAnimationFrame
日志(✅)。 Promise.resolve().then(...)
注册一个微任务 B。
- 宏任务结束: rAF 回调执行完毕。
- 微任务阶段: 清空微任务队列,执行 微任务 B 的回调,控制台输出
requestAnimationFrame - Promise
日志(✅)。 - Tick 2 结束
- 当前队列状态:
- 任务队列:
[M1, M2]
- Idle 队列:
[Idle 回调]
- 任务队列:
- 当前队列状态:
- 宏任务: 事件循环进入下一轮。根据优先级,浏览器准备渲染更新,因此从 rAF 队列 中取出 rAF 宏任务 执行。
Tick 3: 第一个 MessageChannel 消息
- 宏任务: 事件循环从标准任务队列中按先进先出原则取出 宏任务 M1 执行。
channel.port1.onmessage
回调被触发,控制台输出MessageChannel Callback MessageChannel - 1
日志(✅)。
- 宏任务结束: M1 回调执行完毕。
- 微任务阶段: 微任务队列为空,跳过。
- Tick 3 结束
- 当前队列状态:
- 任务队列:
[M2]
- Idle 队列:
[Idle 回调]
- 任务队列:
- 当前队列状态:
- 宏任务: 事件循环从标准任务队列中按先进先出原则取出 宏任务 M1 执行。
Tick 4: 第二个 MessageChannel 消息
- 宏任务: 事件循环继续从标准任务队列中取出 宏任务 M2 执行。
channel.port1.onmessage
回调被触发。- 控制台输出
MessageChannel Callback MessageChannel - 2
日志(✅)。
- 宏任务结束: M2 回调执行完毕。
- 微任务阶段: 微任务队列为空,跳过。
- Tick 4 结束
- 当前队列状态:
- 任务队列:
[]
- Idle 队列:
[Idle 回调]
- 任务队列:
- 当前队列状态:
- 宏任务: 事件循环继续从标准任务队列中取出 宏任务 M2 执行。
Tick 5: 空闲状态回调 (requestIdleCallback)
- 宏任务: 事件循环发现高优先级队列已空,且浏览器处于空闲状态。因此,从 Idle 任务队列 中取出 Idle 宏任务 执行。
- 控制台输出
requestIdleCallback
日志(✅)。 - 执行
Promise.resolve().then(...)
注册一个微任务 C。 - 执行
channel.port2.postMessage({ message: 'MessageChannel - 3' })
,注册一个宏任务 M3 到标准任务队列。
- 控制台输出
- 宏任务结束: Idle 回调执行完毕。
- 微任务阶段: 清空微任务队列。
- 执行 微任务 C 的回调,控制台输出
requestIdleCallback - Promise
日志(✅)。
- 执行 微任务 C 的回调,控制台输出
- Tick 5 结束
- 当前队列状态:
- 任务队列:
[M3]
- Idle 队列:
[]
- 任务队列:
- 当前队列状态:
- 宏任务: 事件循环发现高优先级队列已空,且浏览器处于空闲状态。因此,从 Idle 任务队列 中取出 Idle 宏任务 执行。
Tick 6: 第三个 MessageChannel 消息
- 宏任务: 事件循环从标准任务队列中取出宏任务 M3 执行。
channel.port1.onmessage
回调被触发。- 控制台输出
MessageChannel Callback MessageChannel - 3
日志(✅)。
- 宏任务结束: M3 回调执行完毕。
- 微任务阶段: 微任务队列为空,跳过。
- Tick 6 结束
- 当前队列状态: 所有不同种类的任务队列均为已清空,任务执行完成。
- 宏任务: 事件循环从标准任务队列中取出宏任务 M3 执行。
流程总结
Tick | 宏任务类型 | 控制台输出 |
---|---|---|
1 | 初始脚本 | (无) |
2 | requestAnimationFrame | requestAnimationFrame requestAnimationFrame - Promise |
3 | MessageChannel (第一个) | MessageChannel Callback MessageChannel - 1 |
4 | MessageChannel (第二个) | MessageChannel Callback MessageChannel - 2 |
5 | requestIdleCallback | requestIdleCallback requestIdleCallback - Promise |
6 | MessageChannel (第三个) | MessageChannel Callback MessageChannel - 3 |
接下来将 requestAnimationFrame
和 requestIdleCallback
统称为 环境渲染任务(即浏览器渲染周期 api 调度的任务)。那么对于浏览器宿主环境的任务类型来说,可以划分为 宏任务、微任务、环境渲染任务 三类任务类型,后续的内容就对这三类任务做具体的可行性分析。
可行性分析
首先需要先确定一下是要使用宏任务,还是微任务。
微任务的可行性分析
react
需要具备并发执行的特性,并发就意味着单个事件循环 tick
中只能执行部分任务,剩余的任务分配给后续多个事件循环 tick
中执行。
在单个事件循环 tick
中,是会清空所有的微任务队列。若使用调度采用微任务,那么就需要做到最基本的一件事情是,如何在这一个事件循环 tick
中执行受限微任务,在后续的事件循环 tick
中再执行剩余的受限微任务。
直接在一个事件循环 tick
中推送多个微任务是不切实际的,这些微任务都会被执行掉,这是同步调度而非并发调度。并发的意义在于可以控制一帧任务中控制要执行哪些任务。
那么是否宿主环境中存在能检测事件循环 tick
开始的时机,在这个时机中提前注册要执行的微任务似乎也能满足要求。但不幸的是,宿主环境中并没有提供观测事件循环 tick
开始时机的钩子,这对于 react
来说就无法通过微任务得知下一个事件循环 tick 的时机。
既然没有的话,那么就需要微任务依附在环境 api
,跟随环境 api
的执行时机而触发调度,接下来分析一下环境 api
(requestAnimationFrame
、requestIdleCallback
)。
浏览器渲染任务的可行性分析 (rAF / rIC)
测试用例
let startTime = null;
let exCount = 0;
const handler = function (info) {
return function (count) {
console.log(
`VI-LOG_${info}_${count} -- Time Overhead: `,
new Date().getTime() - startTime
);
};
};
const requestIdleCallbackOnMessage = handler('requestIdleCallback');
const requestAnimationFrameOnMessage = handler('requestAnimationFrame');
const channel = new MessageChannel();
channel.port1.onmessage = function wrap({ data }) {
const { count } = data;
channelOnMessage(count);
};
const triggerMessageChannel = count => {
channel.port2.postMessage({ count });
};
const callback = exCount => {
startTime = new Date().getTime();
requestIdleCallback(requestIdleCallbackOnMessage.bind(null, exCount));
requestAnimationFrame(requestAnimationFrameOnMessage.bind(null, exCount));
};
let setInternalTime = new Date().getTime();
setInterval(() => {
console.log(
`------------------------------------ VI-LOG_Test_Start: ${++exCount} ------------------------------------`
);
console.log(
`VI-LOG_setInterval_${exCount} -- Time Overhead: `,
new Date().getTime() - setInternalTime
);
callback(exCount);
setInternalTime = new Date().getTime();
}, 2000);
requestAnimationFrame
的执行行为:- 前台: 回调函数会在浏览器下一次重绘之前执行。这通常与显示器的刷新率同步(例如
60Hz
对应约16.7ms
一帧)。日志中,requestAnimationFrame
在setInterval
回调之后几毫秒内执行,符合预期。 - 后台: 当标签页不是激活状态时,浏览器会暂停
requestAnimationFrame
的回调执行。所有通过requestAnimationFrame
调度的回调会被加入队列,直到标签页恢复激活状态。 - 重新聚焦: 一旦标签页重新激活,所有在后台期间累积的
requestAnimationFrame
回调会在短时间内集中执行完毕,然后再开始处理新的requestAnimationFrame
请求。 - 可行性分析
- 后台 tab 暂停:当
tab
进入后台时,requestAnimationFrame
的回调会完全暂停执行。若react
的核心工作循环完全依赖requestAnimationFrame
,那么在tab
后台时,所有react
更新(包括 状态更新、副作用处理 等)都会停止。这对于某些需要后台保持一定活性的应用(如 消息通知、数据预取后更新状态 等场景)是不可接受的。 - 用途:
requestAnimationFrame
调度完成后会执行浏览器渲染工作,这确实适合react
的commit
阶段的同步任务。但react
并不仅有与渲染相关的任务,还存在大量副作用的任务,对于这些副作用的任务的处理放置在requestAnimationFrame
中并不合理。
- 后台 tab 暂停:当
- 前台: 回调函数会在浏览器下一次重绘之前执行。这通常与显示器的刷新率同步(例如
requestIdleCallback
的执行行为:前台: 回调函数会在浏览器主线程处于空闲时期时被调用。这允许执行一些低优先级的后台任务而不会影响用户体验(如动画流畅性)。
后台:
requestIdleCallback
的行为与requestAnimationFrame
不同。即使在后台,如果浏览器判断有空闲资源,requestIdleCallback
的回调仍然可能被执行。不过,后台requestIdleCallback
的执行频率和可用的timeRemaining()
可能会受到限制。重新聚焦: 类似于
requestAnimationFrame
,如果在后台期间有requestIdleCallback
回调被调度但因某些原因(如没有足够的空闲时间或浏览器策略)未能立即执行,它们也可能在Tab
重新激活后得到执行。可行性分析
后台 tab 暂停:
requestIdleCallback
在后台tab
中仍有机会执行,尽管频率和可靠性可能降低。这比requestAnimationFrame
的完全暂停要好,requestIdleCallback
执行时机是不稳定的,只有在浏览器空闲时才会执行,可以视作低优先级任务,容易被浏览器其他任务占用主线程而导致饥饿问题。这对于需要及时响应的用户交互(如输入框反馈)或高优先级更新是不可接受的。利用空闲时间:
requestIdleCallback
允许在浏览器主线程空闲时执行任务,这其实是符合react
的并发可中断渲染。同时还提供deadline.timeRemaining()
机制,有利于协助react
判断当前帧有多少剩余时间可用于执行工作。兼容性问题:
safari v18.5
版本默认是关闭requestIdleCallback
特性,IE
、Opera Mini
完全不支持。用户采用率仅78.95%
。Hack 行为:
requestIdleCallback
可以通过配置timeout
参数来使低优先任务超时而推送到宿主环境的宏任务队列。与此同时,经测试发现当配置timeout > 0
时(timeout = 0
时,后台任务会堆积后聚焦时一次性执行),后台执行触发的时机也相对稳定。因此若场景中无需考虑
requestIdleCallback
的兼容性和跨平台问题,通过requestIdleCallback(fn, { timeout: 1 })
也是可以在一定程度上完成调度任务。但这是一种
hack
行为,与requestIdleCallback
的设计初衷相悖。requestIdleCallback
的设计初衷是将任务视为低优先级任务调度,当宿主环境空闲时或任务饥饿时进行调度,而requestIdleCallback(fn, { timeout: 1 })
的意义则视为尽快将任务推送到宿主环境的宏任务队列中,等待宿主环境调度。react
希望能够 快速响应并执行其内部最高优先级任务,虽然能通过hack
行为来完成调度,但timeout: 1
推送宏任务的积极性并没有setTimeout(fn, 0)
和new MessageChannel()
高。与此同时hack
方式与requestIdleCallback
的初衷相悖,再接着requestIdleCallback
又存在兼容性和跨平台性问题,因此选择采纳requestIdleCallback
并不是一个很好的选择。
综上分析可知,requestAnimationFrame
和 requestIdleCallback
并不适合作为 react
通用场景下的任务调度器。讨论到此,宿主环境任务 以及 微任务 均不适合。
那么接下来就对宏任务进行分析吧。对于 react
来说是需要一个稳定且高效的调度机制,宏任务本身就适合作为调度机制。每一次事件循环 tick
中,浏览器都会从任务队列中取一个宏任务执行,这个过程通常情况下是稳定且可控的。
那么接下来就对宏任务进行分析吧。
宏任务的可行性分析
测试用例
let startTime = null;
let exCount = 0;
const handler = function (info) {
return function (count) {
console.log(
`VI-LOG_${info}_${count} -- Time Overhead: `,
new Date().getTime() - startTime
);
};
};
const channelOnMessage = handler('MessageChannel');
const setTimeoutPreOnMessage = handler('setTimeout pre');
const setTimeoutPostOnMessage = handler('setTimeout post');
const channel = new MessageChannel();
channel.port1.onmessage = function wrap({ data }) {
const { count } = data;
channelOnMessage(count);
};
const triggerMessageChannel = count => {
channel.port2.postMessage({ count });
};
const callback = exCount => {
startTime = new Date().getTime();
setTimeout(setTimeoutPreOnMessage.bind(null, exCount), 0);
triggerMessageChannel(exCount);
setTimeout(setTimeoutPostOnMessage.bind(null, exCount), 0);
};
let setInternalTime = new Date().getTime();
setInterval(() => {
console.log(
`------------------------------------ VI-LOG_Test_Start: ${++exCount} ------------------------------------`
);
console.log(
`VI-LOG_setInterval_${exCount} -- Time Overhead: `,
new Date().getTime() - setInternalTime
);
callback(exCount);
setInternalTime = new Date().getTime();
}, 2000);
根据图表可分析出,在后台需要持续进行精确、低延迟任务的场景中,setTimeout
和 setInterval
是不可靠的,会受到浏览器节流策略的严重影响。而 MessageChannel
作为一种宏任务,无论标签页是在前台还是后台,MessageChannel
的时间开销始终保持在 0-2ms
之间,表现是三者中最稳定、最高效的。因此 MessageChannel
是实现可靠、高性能任务调度的理想选择。
注意
浏览器在标签页退后台后会对 setTimeout(fn, 0)
进行显著的节流(throttling
),导致其延迟通常增加到约 1s
左右,当处于后台的时间达到 12.4min
左右,会将延迟时间提升到 1min
左右。相比之下,MessageChannel
的消息传递即便在标签页后台状态下,其回调执行相对稳定,虽然在非激活 tab
场景下偶尔会存在回调延迟问题(超过 1s
),但基本稳定在 10ms
以下。标签页重新激活后,setTimeout
的响应会恢复。
那么接下来分析一下 react
是如何选择调度机制的。
选择策略
模拟 React 的调度机制
我们来简单模拟一下 react
的调度机制:
import { createSumTasks } from './src/task.mjs';
import { runConcurrent, runSync } from './src/reconciler.mjs';
import { SchedulerType } from './src/constants.mjs';
const tasks = createSumTasks(50);
const runTests = async () => {
runSync(tasks);
const runTest = schedulerType => {
return new Promise(resolve => {
console.log(`\n--- Testing [${schedulerType}] ---`);
console.time(`${schedulerType} Total Time`);
runConcurrent(tasks, schedulerType, () => {
console.timeEnd(`${schedulerType} Total Time`);
resolve();
});
});
};
await runTest(SchedulerType.MessageChannel);
await runTest(SchedulerType.SetTimeout);
if (typeof setImmediate !== 'undefined') {
await runTest(SchedulerType.SetImmediate);
} else {
console.log(
'\n--- Skipping SetImmediate test (not available in browser) ---'
);
}
};
runTests();
class TaskContainer {
constructor() {
this.tasks = [];
}
peek() {
return this.tasks[0];
}
pop() {
this.tasks.shift();
}
push(task) {
this.tasks.push(task);
}
isEmpty() {
return this.tasks.length === 0;
}
}
const createTask = taskCount => {
console.time(`Task ${taskCount + 1} Execution Time`);
let sum = 0;
for (let i = 0; i < 2000000; ++i) sum += i;
console.timeEnd(`Task ${taskCount + 1} Execution Time`);
return sum;
};
export const createSumTasks = count =>
Array(count)
.fill(1)
.map((_, index) => createTask.bind(null, index));
export const taskContainer = new TaskContainer();
import { getScheduler, scheduleCallback } from './scheduler.mjs';
import { SchedulerType } from './constants.mjs';
const timeSlice = 5;
let pendingTask = null;
function workLoop(deadline, delay) {
const scheduler = getScheduler();
console.log(
`\nStarting work loop with ${scheduler.type}, callback delay: ${delay}ms`
);
let shouldYield = false;
while (pendingTask && !shouldYield) {
pendingTask.handler[pendingTask.currentIndex]();
pendingTask.currentIndex++;
if (pendingTask.currentIndex >= pendingTask.handler.length) {
pendingTask = null;
console.log(`All tasks finished with ${scheduler.type}.`);
if (scheduler.onComplete) scheduler.onComplete();
return;
}
shouldYield = deadline.timeRemaining() < 1;
}
if (pendingTask) {
console.log('Yielding...');
scheduler.schedule(performWorkUntilDeadline);
}
}
function performWorkUntilDeadline(delay) {
const startTime = performance.now();
const deadline = {
timeRemaining: () =>
Math.max(0, timeSlice - (performance.now() - startTime))
};
workLoop(deadline, delay);
}
export function runConcurrent(tasks, schedulerType, onComplete) {
pendingTask = {
handler: tasks,
currentIndex: 0
};
scheduleCallback(({ callbackTime }) => {
const delay = new Date().getTime() - callbackTime;
performWorkUntilDeadline(delay);
}, schedulerType);
const scheduler = getScheduler();
scheduler.onComplete = onComplete;
}
export function runSync(tasks) {
console.log(`\n--- Testing [Synchronous] ---\n`);
console.time('Synchronous Total Time');
for (const task of tasks) {
task();
}
console.timeEnd('Synchronous Total Time');
}
import { SchedulerType } from './constants.mjs';
let scheduler;
export const getScheduler = () => scheduler;
export const scheduleCallback = (
callback,
type = SchedulerType.MessageChannel
) => {
const schedule = () => {
switch (type) {
case SchedulerType.MessageChannel: {
const channel = new MessageChannel();
channel.port1.onmessage = event => {
callback(event.data);
channel.port1.close();
};
channel.port2.postMessage({ callbackTime: new Date().getTime() });
break;
}
case SchedulerType.SetTimeout: {
setTimeout(
callback.bind(null, { callbackTime: new Date().getTime() }),
0
);
break;
}
case SchedulerType.SetImmediate: {
setImmediate(
callback.bind(null, { callbackTime: new Date().getTime() })
);
break;
}
default:
break;
}
};
scheduler = {
type,
schedule
};
schedule();
};
export const SchedulerType = {
MessageChannel: 'MessageChannel',
SetTimeout: 'SetTimeout',
SetImmediate: 'SetImmediate'
};
浏览器宿主环境下的输出日志
--- Testing [Synchronous] ---
Task 1 Execution Time: 3.722900390625 ms
Task 2 Execution Time: 2.032958984375 ms
Task 3 Execution Time: 2.031005859375 ms
Task 4 Execution Time: 2.037109375 ms
Task 5 Execution Time: 2.046142578125 ms
Task 6 Execution Time: 2.02587890625 ms
Task 7 Execution Time: 2.134033203125 ms
Task 8 Execution Time: 1.98486328125 ms
Task 9 Execution Time: 1.9970703125 ms
Task 10 Execution Time: 2.01806640625 ms
Task 11 Execution Time: 2.010009765625 ms
Task 12 Execution Time: 1.975830078125 ms
Task 13 Execution Time: 1.9619140625 ms
Task 14 Execution Time: 1.989013671875 ms
Task 15 Execution Time: 2.037109375 ms
Task 16 Execution Time: 1.97216796875 ms
Task 17 Execution Time: 1.954833984375 ms
Task 18 Execution Time: 1.916015625 ms
Task 19 Execution Time: 1.902099609375 ms
Task 20 Execution Time: 1.88916015625 ms
Task 21 Execution Time: 1.89404296875 ms
Task 22 Execution Time: 1.907958984375 ms
Task 23 Execution Time: 1.931884765625 ms
Task 24 Execution Time: 2.009033203125 ms
Task 25 Execution Time: 1.889892578125 ms
Task 26 Execution Time: 1.89599609375 ms
Task 27 Execution Time: 1.9248046875 ms
Task 28 Execution Time: 1.986083984375 ms
Task 29 Execution Time: 1.976806640625 ms
Task 30 Execution Time: 1.9228515625 ms
Task 31 Execution Time: 1.89501953125 ms
Task 32 Execution Time: 1.9970703125 ms
Task 33 Execution Time: 1.989013671875 ms
Task 34 Execution Time: 1.887939453125 ms
Task 35 Execution Time: 1.887939453125 ms
Task 36 Execution Time: 1.89794921875 ms
Task 37 Execution Time: 1.93798828125 ms
Task 38 Execution Time: 1.907958984375 ms
Task 39 Execution Time: 1.899169921875 ms
Task 40 Execution Time: 1.89306640625 ms
Task 41 Execution Time: 2.011962890625 ms
Task 42 Execution Time: 2.01220703125 ms
Task 43 Execution Time: 2.010009765625 ms
Task 44 Execution Time: 2.0029296875 ms
Task 45 Execution Time: 1.90087890625 ms
Task 46 Execution Time: 1.923828125 ms
Task 47 Execution Time: 1.90283203125 ms
Task 48 Execution Time: 1.89111328125 ms
Task 49 Execution Time: 2.001953125 ms
Task 50 Execution Time: 1.98291015625 ms
Synchronous Total Time: 100.989990234375 ms
--- Testing [MessageChannel] ---
Starting work loop with MessageChannel, callback delay: 2ms
Task 1 Execution Time: 1.947998046875 ms
Task 2 Execution Time: 1.92822265625 ms
Task 3 Execution Time: 1.93212890625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 4 Execution Time: 1.9599609375 ms
Task 5 Execution Time: 2.025146484375 ms
Task 6 Execution Time: 2.123046875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 7 Execution Time: 1.966064453125 ms
Task 8 Execution Time: 1.983154296875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 9 Execution Time: 1.97412109375 ms
Task 10 Execution Time: 1.906005859375 ms
Task 11 Execution Time: 1.929931640625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 12 Execution Time: 1.900146484375 ms
Task 13 Execution Time: 1.9208984375 ms
Task 14 Execution Time: 2.090087890625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 15 Execution Time: 1.9140625 ms
Task 16 Execution Time: 1.90283203125 ms
Task 17 Execution Time: 1.890869140625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 18 Execution Time: 1.90478515625 ms
Task 19 Execution Time: 1.890869140625 ms
Task 20 Execution Time: 1.89111328125 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 21 Execution Time: 1.906982421875 ms
Task 22 Execution Time: 1.945068359375 ms
Task 23 Execution Time: 1.9970703125 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 24 Execution Time: 1.906982421875 ms
Task 25 Execution Time: 1.971923828125 ms
Task 26 Execution Time: 1.985107421875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 27 Execution Time: 2.0439453125 ms
Task 28 Execution Time: 2.053955078125 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 29 Execution Time: 2.033935546875 ms
Task 30 Execution Time: 2.0400390625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 31 Execution Time: 3.156982421875 ms
Task 32 Execution Time: 3.77294921875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 33 Execution Time: 2.02099609375 ms
Task 34 Execution Time: 1.968994140625 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 35 Execution Time: 1.958984375 ms
Task 36 Execution Time: 2.0009765625 ms
Task 37 Execution Time: 2.072998046875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 38 Execution Time: 2.02001953125 ms
Task 39 Execution Time: 2.01513671875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 40 Execution Time: 2.019775390625 ms
Task 41 Execution Time: 2.01904296875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 42 Execution Time: 2.02392578125 ms
Task 43 Execution Time: 2.024169921875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 44 Execution Time: 2.016845703125 ms
Task 45 Execution Time: 2.0849609375 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 46 Execution Time: 1.9599609375 ms
Task 47 Execution Time: 1.948974609375 ms
Task 48 Execution Time: 1.91796875 ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 49 Execution Time: 1.906982421875 ms
Task 50 Execution Time: 1.88916015625 ms
All tasks finished with MessageChannel.
MessageChannel Total Time: 108.123046875 ms
--- Testing [SetTimeout] ---
Starting work loop with SetTimeout, callback delay: 0ms
Task 1 Execution Time: 1.93896484375 ms
Task 2 Execution Time: 1.89013671875 ms
Task 3 Execution Time: 1.998046875 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 4 Execution Time: 1.9169921875 ms
Task 5 Execution Time: 1.9208984375 ms
Task 6 Execution Time: 1.891845703125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 7 Execution Time: 1.89892578125 ms
Task 8 Execution Time: 1.928955078125 ms
Task 9 Execution Time: 1.885986328125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 10 Execution Time: 1.912109375 ms
Task 11 Execution Time: 1.964111328125 ms
Task 12 Execution Time: 2.014892578125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 13 Execution Time: 1.944091796875 ms
Task 14 Execution Time: 1.964111328125 ms
Task 15 Execution Time: 1.989013671875 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 16 Execution Time: 1.93115234375 ms
Task 17 Execution Time: 1.89111328125 ms
Task 18 Execution Time: 1.902099609375 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 19 Execution Time: 1.89990234375 ms
Task 20 Execution Time: 1.884033203125 ms
Task 21 Execution Time: 1.89306640625 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 22 Execution Time: 2.032958984375 ms
Task 23 Execution Time: 1.953125 ms
Task 24 Execution Time: 1.962890625 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 25 Execution Time: 1.949951171875 ms
Task 26 Execution Time: 2.08203125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 27 Execution Time: 2.31982421875 ms
Task 28 Execution Time: 2.2958984375 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 29 Execution Time: 2.325927734375 ms
Task 30 Execution Time: 2.191162109375 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 31 Execution Time: 2.208984375 ms
Task 32 Execution Time: 2.177978515625 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 33 Execution Time: 2.177001953125 ms
Task 34 Execution Time: 2.201904296875 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 35 Execution Time: 2.051025390625 ms
Task 36 Execution Time: 2.031982421875 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 37 Execution Time: 2.202880859375 ms
Task 38 Execution Time: 2.169189453125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 39 Execution Time: 2.177978515625 ms
Task 40 Execution Time: 2.169921875 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 41 Execution Time: 2.280029296875 ms
Task 42 Execution Time: 2.18798828125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 43 Execution Time: 2.175048828125 ms
Task 44 Execution Time: 2.051025390625 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 4ms
Task 45 Execution Time: 2.02001953125 ms
Task 46 Execution Time: 2.01611328125 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 47 Execution Time: 2.033935546875 ms
Task 48 Execution Time: 2.011962890625 ms
Yielding...
Starting work loop with SetTimeout, callback delay: 5ms
Task 49 Execution Time: 2.18212890625 ms
Task 50 Execution Time: 2.128173828125 ms
All tasks finished with SetTimeout.
SetTimeout Total Time: 174.214111328125 ms
Node.js 宿主环境下的输出日志
--- Testing [Synchronous] ---
Task 1 Execution Time: 25.098ms
Task 2 Execution Time: 8.421ms
Task 3 Execution Time: 2.149ms
Task 4 Execution Time: 8.976ms
Task 5 Execution Time: 8.824ms
Task 6 Execution Time: 10.105ms
Task 7 Execution Time: 9.672ms
Task 8 Execution Time: 8.75ms
Task 9 Execution Time: 9.937ms
Task 10 Execution Time: 9.788ms
Task 11 Execution Time: 8.357ms
Task 12 Execution Time: 7.754ms
Task 13 Execution Time: 9.477ms
Task 14 Execution Time: 8.105ms
Task 15 Execution Time: 8.901ms
Task 16 Execution Time: 8.298ms
Task 17 Execution Time: 14.061ms
Task 18 Execution Time: 8.366ms
Task 19 Execution Time: 7.509ms
Task 20 Execution Time: 7.745ms
Task 21 Execution Time: 8.298ms
Task 22 Execution Time: 7.858ms
Task 23 Execution Time: 7.284ms
Task 24 Execution Time: 7.919ms
Task 25 Execution Time: 7.282ms
Task 26 Execution Time: 7.33ms
Task 27 Execution Time: 7.639ms
Task 28 Execution Time: 7.228ms
Task 29 Execution Time: 7.558ms
Task 30 Execution Time: 7.242ms
Task 31 Execution Time: 7.658ms
Task 32 Execution Time: 7.235ms
Task 33 Execution Time: 7.744ms
Task 34 Execution Time: 7.721ms
Task 35 Execution Time: 10.821ms
Task 36 Execution Time: 7.562ms
Task 37 Execution Time: 9.365ms
Task 38 Execution Time: 7.5ms
Task 39 Execution Time: 7.602ms
Task 40 Execution Time: 7.309ms
Task 41 Execution Time: 7.983ms
Task 42 Execution Time: 7.388ms
Task 43 Execution Time: 7.558ms
Task 44 Execution Time: 7.704ms
Task 45 Execution Time: 7.191ms
Task 46 Execution Time: 7.644ms
Task 47 Execution Time: 7.99ms
Task 48 Execution Time: 8.627ms
Task 49 Execution Time: 7.243ms
Task 50 Execution Time: 8.281ms
Sync Total Time: 432.438ms
--- Testing [MessageChannel] ---
Starting work loop with MessageChannel, callback delay: 28ms
Task 1 Execution Time: 7.841ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 2 Execution Time: 8.266ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 3 Execution Time: 7.381ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 4 Execution Time: 7.662ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 5 Execution Time: 7.523ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 6 Execution Time: 8.608ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 7 Execution Time: 8.118ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 8 Execution Time: 8.009ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 9 Execution Time: 7.349ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 10 Execution Time: 8.054ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 11 Execution Time: 7.714ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 12 Execution Time: 7.868ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 13 Execution Time: 7.411ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 14 Execution Time: 7.637ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 15 Execution Time: 7.501ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 16 Execution Time: 7.755ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 17 Execution Time: 7.54ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 18 Execution Time: 7.451ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 19 Execution Time: 8.255ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 20 Execution Time: 8.167ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 21 Execution Time: 7.676ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 22 Execution Time: 7.389ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 23 Execution Time: 7.835ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 24 Execution Time: 7.381ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 25 Execution Time: 7.723ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 26 Execution Time: 7.325ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 27 Execution Time: 7.926ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 28 Execution Time: 7.694ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 29 Execution Time: 7.74ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 30 Execution Time: 7.509ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 31 Execution Time: 8.088ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 32 Execution Time: 8.238ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 33 Execution Time: 10.393ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 34 Execution Time: 55.186ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 35 Execution Time: 9.902ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 36 Execution Time: 10.968ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 37 Execution Time: 7.874ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 38 Execution Time: 9.751ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 39 Execution Time: 8.158ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 40 Execution Time: 8.687ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 41 Execution Time: 10.244ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 42 Execution Time: 17.302ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 43 Execution Time: 14.435ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 44 Execution Time: 93.865ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 45 Execution Time: 29.792ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 46 Execution Time: 13.411ms
Yielding...
Starting work loop with MessageChannel, callback delay: 1ms
Task 47 Execution Time: 28.059ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 48 Execution Time: 12.793ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 49 Execution Time: 10.047ms
Yielding...
Starting work loop with MessageChannel, callback delay: 0ms
Task 50 Execution Time: 12.102ms
All tasks finished with MessageChannel.
MessageChannel Total Time: 661.233ms
--- Testing [SetTimeout] ---
Starting work loop with SetTimeout, callback delay: 1ms
Task 1 Execution Time: 13.236ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 2 Execution Time: 10.056ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 3 Execution Time: 11.915ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 4 Execution Time: 11.289ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 5 Execution Time: 9.037ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 6 Execution Time: 11.884ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 7 Execution Time: 11.763ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 8 Execution Time: 11.801ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 9 Execution Time: 10.262ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 10 Execution Time: 8.771ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 11 Execution Time: 8.516ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 12 Execution Time: 13.238ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 13 Execution Time: 7.923ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 14 Execution Time: 8.7ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 15 Execution Time: 7.922ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 16 Execution Time: 8.342ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 17 Execution Time: 7.938ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 18 Execution Time: 8.889ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 19 Execution Time: 7.805ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 20 Execution Time: 7.868ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 21 Execution Time: 8.572ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 22 Execution Time: 9.975ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 23 Execution Time: 9.685ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 24 Execution Time: 17.216ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 25 Execution Time: 7.561ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 26 Execution Time: 8.147ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 27 Execution Time: 8.373ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 28 Execution Time: 7.816ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 29 Execution Time: 7.72ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 30 Execution Time: 7.241ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 31 Execution Time: 7.554ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 32 Execution Time: 8.101ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 33 Execution Time: 7.386ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 34 Execution Time: 7.318ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 35 Execution Time: 7.319ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 36 Execution Time: 7.363ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 37 Execution Time: 7.359ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 38 Execution Time: 7.599ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 39 Execution Time: 7.448ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 40 Execution Time: 7.664ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 41 Execution Time: 7.597ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 42 Execution Time: 8.319ms
Yielding...
Starting work loop with SetTimeout, callback delay: 0ms
Task 43 Execution Time: 8.281ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 44 Execution Time: 8.249ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 45 Execution Time: 7.494ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 46 Execution Time: 7.404ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 47 Execution Time: 7.229ms
Yielding...
Starting work loop with SetTimeout, callback delay: 1ms
Task 48 Execution Time: 7.44ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 49 Execution Time: 7.521ms
Yielding...
Starting work loop with SetTimeout, callback delay: 2ms
Task 50 Execution Time: 7.396ms
All tasks finished with SetTimeout.
SetTimeout Total Time: 508.129ms
--- Testing [SetImmediate] ---
Starting work loop with SetImmediate, callback delay: 0ms
Task 1 Execution Time: 7.276ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 2 Execution Time: 7.467ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 3 Execution Time: 7.173ms
Yielding...
Starting work loop with SetImmediate, callback delay: 1ms
Task 4 Execution Time: 7.425ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 5 Execution Time: 7.31ms
Yielding...
Starting work loop with SetImmediate, callback delay: 1ms
Task 6 Execution Time: 8.497ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 7 Execution Time: 7.269ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 8 Execution Time: 7.528ms
Yielding...
Starting work loop with SetImmediate, callback delay: 1ms
Task 9 Execution Time: 7.353ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 10 Execution Time: 7.614ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 11 Execution Time: 7.481ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 12 Execution Time: 7.268ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 13 Execution Time: 7.728ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 14 Execution Time: 7.278ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 15 Execution Time: 7.477ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 16 Execution Time: 7.212ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 17 Execution Time: 7.504ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 18 Execution Time: 7.242ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 19 Execution Time: 8.966ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 20 Execution Time: 7.295ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 21 Execution Time: 7.546ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 22 Execution Time: 7.217ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 23 Execution Time: 7.348ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 24 Execution Time: 7.317ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 25 Execution Time: 7.22ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 26 Execution Time: 7.755ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 27 Execution Time: 7.24ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 28 Execution Time: 7.55ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 29 Execution Time: 7.29ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 30 Execution Time: 7.931ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 31 Execution Time: 7.306ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 32 Execution Time: 9.013ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 33 Execution Time: 7.68ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 34 Execution Time: 8.519ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 35 Execution Time: 7.398ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 36 Execution Time: 7.54ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 37 Execution Time: 7.276ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 38 Execution Time: 8.048ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 39 Execution Time: 7.404ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 40 Execution Time: 7.46ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 41 Execution Time: 7.311ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 42 Execution Time: 7.18ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 43 Execution Time: 7.529ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 44 Execution Time: 7.226ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 45 Execution Time: 7.434ms
Yielding...
Starting work loop with SetImmediate, callback delay: 1ms
Task 46 Execution Time: 8.103ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 47 Execution Time: 7.366ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 48 Execution Time: 7.148ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 49 Execution Time: 7.454ms
Yielding...
Starting work loop with SetImmediate, callback delay: 0ms
Task 50 Execution Time: 7.28ms
All tasks finished with SetImmediate.
SetImmediate Total Time: 384.324ms
分析模拟调度器在 node.js
和浏览器环境下的表现:
setImmediate
任务从调度到触发的时间几乎都是
0ms
,具备稳定性。可见setImmediate
就如名字一样,立即执行 并符合 语义化。setTimeout
不管是
node.js
还是浏览器环境下,setTimeout
的执行时间都存在波动。当setTimeout
配置为0ms
且处于聚焦tab
场景下,setTimeout
呈现的依旧是延迟执行的特性,特别是在多层嵌套调用时,延迟稳定在4ms
左右。换句话说,在客户端场景下(客户端渲染或客户端注水),若通过setTimeout(..., 0)
来实现react
的调度,当任务数量多于5
个时,就需要额外支付4ms
的延迟成本,这并不符合react
的高效调度策略。在非聚焦场景下,
setTimeout
还呈现出延迟低频率调度的现象,影响后台场景下的调度效率。MessageChannel
调度器在
MessageChannel
的调度下,执行时间几乎都是0ms
,具备稳定性。不过在node.js
环境下,MessageChannel
首次执行时间相对较长,后续没有任务时并不会结束进程,需要手动关闭MessageChannel
的port
才能结束进程。
虽然上述调度器在 node.js
和浏览器环境下表现各异,但其实都可以作为 react
的调度器,只不过在部分场景下表现存在差异。
由上分析我们可以简单做一下调度器选择优先级:
setImmediate
(node.js) > MessageChannel
(浏览器) > setTimeout
(浏览器)
那么 react
是如何选择调度器的呢?
React 的选择策略
react
在 scheduler
包中也是采用宏任务作为任务调度,代码如下:
// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout =
typeof setTimeout === 'function' ? setTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
const performWorkUntilDeadline = () => {
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);
};
}
};
从代码注释和代码逻辑可以看出,react
选择的优先级顺序是:setImmediate
> MessageChannel
> setTimeout
,这与我们上文分析的优先级顺序一致。
通用 API 调度的局限性
无法区分任务优先级
存在的问题:通用 API
(MessageChannel
、setTimeout
) 只能以单一的、相同的优先级来调度所有任务。每次选择要推送哪一个任务时,react scheduler
内部小顶堆实现方式会根据任务的优先级来选择最高或紧急优先级任务,是在用户态模拟优先级调度。任务会交付给通用 API
来执行,但通用 API
无法区分任务的优先级,只能以相同的优先级来调度所有任务,也就是说 MessageChannel(task_A)
和 MessageChannel(task_B)
执行的任务优先级是一样的。
解决方案:postTask
允许为任务指定不同的优先级(user-blocking
, user-visible
, background
)。本质上是浏览器作为 全局调度者,会根据调度方提供的优先级在合适的时机处理。换句话说,浏览器可以在一个 全局的视野 下进行真正的 抢占式调度,而 react scheduler
的视野仅限于自己内部小顶堆模拟的任务队列,这是二者最本质的区别。
低效任务中断与恢复
存在的问题:现阶段 react
通过配置 5ms
的阈值来实现时间切片,每次任务执行时都会进行判断是否已经超过了阈值,若超过则主动让出控制权,停止执行 react
后续的任务,将主线程的控制权交付给宿主环境,剩余的任务留给下一次事件循环 Tick
中执行。
const frameYieldMs = 5;
const frameInterval = frameYieldMs;
function shouldYieldToHost(): boolean {
if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
// Yield now.
return true;
}
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;
}
// Yield now.
return true;
}
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;
}
}
}
};
5ms
是 react
团队根据实验得出最适中的时间,在通常情况下是合理时间,但宿主环境的调度是复杂的,一帧中适合任务执行的时间是动态的,可能并没有更高优先级的任务等待处理,此时让出主线程的控制权并在其他事件循环 Tick
中恢复执行,并不是一件高效的事情。
解决方案:postTask
可以让浏览器来管理任务的优先级,任务内部调用 await scheduler.yield()
,让浏览器决定当前是否有其他高优的任务而让出主线程的控制权。浏览器会保存任务的上下文,处理完高优的任务可以立刻恢复原先任务的执行上下文,更高效的完成任务。
const task = async () => {
// ...
};
const sumTasks = Array(1000).fill(task);
const longTask = async () => {
for (const task of sumTasks) {
await task();
await scheduler.yield();
}
};
scheduler.postTask(longTask, { priority: 'background' });
当执行长任务时,通过 scheduler.yield()
来让浏览器决策是否有高优先级的任务待处理,若有则让出主线程的控制权,浏览器处理完高优先级的任务后,会立刻恢复原先任务的执行,继续后续的 for ... of
的任务处理。
scheduler.postTask
和 scheduler.yield()
是对 shouldYieldToHost
逻辑的完善和高效 react
内部任务恢复的逻辑,将决策者从 react 的预测 变为了 浏览器的全知视角,浏览器是真的知道此刻有没有更高优的任务需要处理,从而做出符合实际的决策。
中止任务
由上可知,react scheduler
现阶段采用 setImmediate
、MessageChannel
、setTimeout
作为任务调度器,而这些调度器都存在一个共同的问题,那就是无法取消任务。那么 react scheduler
是如何解决这个问题的呢?
function unstable_cancelCallback(task: Task) {
if (enableProfiling) {
if (task.isQueued) {
const currentTime = getCurrentTime();
markTaskCanceled(task, currentTime);
task.isQueued = false;
}
}
// Null out the callback to indicate the task has been canceled. (Can't
// remove from the queue because you can't remove arbitrary nodes from an
// array based heap, only the first one.)
task.callback = null;
}
unstable_cancelCallback
是 react scheduler
内部用于取消任务的函数,通过 task.callback = null
来取消任务。
这会作用于 timerQueue
和 taskQueue
中,在 timerQueue
中:
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);
}
}
当 timer.callback === null
时,表示任务已经被取消,会直接调用 pop(timerQueue)
来移除任务,连进入 taskQueue
的机会都没有。
在 taskQueue
中:
function workLoop(initialTime: number) {
currentTask = peek(taskQueue);
while (currentTask !== null) {
const callback = currentTask.callback;
if (typeof callback === 'function') {
// ...
} else {
pop(taskQueue);
}
}
}
当 typeof callback !== 'function'
时,意味着任务已经被取消,会直接调用 pop(taskQueue)
来移除任务。
若任务执行途中让出控制权的同时还有其余任务要处理,那么 react scheduler
会将后续的任务作为新的任务赋值,并发起新的调度 performWorkUntilDeadline
任务。
function workLoop(initialTime: number) {
currentTask = peek(taskQueue);
while (currentTask !== null) {
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout =
currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
return true;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
}
}
}
const performWorkUntilDeadline = () => {
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;
}
}
}
react scheduler
将 task.callback
是否有函数视作任务是否要执行的依据,若 task.callback
为 null
则表示任务已经被取消,不会执行。task.callback
的任务是一次性的,任务执行之前就会将 task.callback
赋值为 null
,任务执行途中让出控制权的同时若还有其余任务要处理,那么并不会销毁该 task
,而是将剩余的任务作为新的任务赋值给 task.callback
,并发起新一轮的调度 performWorkUntilDeadline
任务。
scheduler.postTask
可以通过配合 TaskController
来实现原生任务中止的需求
const controller = new TaskController({ priority: postTaskPriority });
const postTaskOptions = {
delay:
typeof options === 'object' && options !== null ? options.delay : 0,
signal: controller.signal
};
scheduler
.postTask(
runTask.bind(null, priorityLevel, postTaskPriority, node, callback),
postTaskOptions
)
.catch(handleAbortError);
通过 controller.abort()
即可取消 callback
的任务
export function unstable_cancelCallback(node: CallbackNode) {
const controller = node._controller;
controller.abort();
}
react scheduler
内部已经模拟了 TaskController
的 abort
机制,那么将任务中止机制交付给浏览器来进行管理的优势是什么呢?
性能与效率
- 更深层次的优化:浏览器可以在更低的层面管理和中止任务。当一个任务被中止时,浏览器可以真正地取消底层的操作(如网络请求、文件读取等),从而节省
cpu
、内存
和网络
资源。 - 避免不必要的
javascript
执行:react scheduler
的模拟机制本质上还是在javascript
层面运行。通过检查标志位(task.callback === null
)来决定是否执行下一个任务片段。虽然高效,但这仍然需要在javascript
引擎中进行调度和判断。而原生AbortController
可以直接通知浏览器内核停止相关活动,减少了不必要的脚本执行和潜在的事件循环拥塞。
- 更深层次的优化:浏览器可以在更低的层面管理和中止任务。当一个任务被中止时,浏览器可以真正地取消底层的操作(如网络请求、文件读取等),从而节省
复杂问题的可靠性
统一的信号传递:
AbortSignal
提供了一个清晰且一致的方式来传递 中止 信号。一个AbortSignal
可以被传递给多个不同的异步操作,当调用abort()
时,所有关联的操作都会收到信号。模式清晰明了,易于理解和维护,降低了手动管理多个取消标志位的复杂性。原子性:浏览器的中止信号传递和状态变更是原子性的,能更可靠地处理并发场景。模拟可能会在复杂的情况下出现竞态条件或状态不一致的问题。
注意
AbortSignal
的核心价值在于 管理和优化宿主环境的资源,是一个 纯前端/客户端 的机制。
AbortSignal
在大部分场景下是无法完全阻止已经发生的副作用,前端通过考虑大量的边界问题来维护已经拒绝请求的副作用。例如 fetch
发送请求给后端处理,AbortSignal
无法告知服务器请求已经被中止,服务器会继续处理请求,直到请求结束;用户虽可以通过快速的通讯方式将中止的消息告知服务器,但也无法阻止浏览器需要耗费资源来下载请求的响应数据、应用层需要区分获取的数据是否是失效的、应用销毁后对数据的处理等问题。
AbortSignal
机制的本质是 通知 宿主环境这是一个无效的任务,宿主环境层面会协助中止任务的同时优化副作用对宿主环境的影响,避免无效的 cpu
占用以及内存泄漏问题。
Scheduler API 存在的问题
浏览器兼容性差
- 支持:主要在基于
chromium
的浏览器中(chrome
、edge
、opera
)自版本94
左右开始支持。 - 不支持:
firefox
最新Nightly
版本142
开始支持,safari
完全不支持。
- 支持:主要在基于
chromium
的浏览器中(chrome
、edge
、opera
)自版本129
左右开始支持。 - 不支持:
firefox
最新Nightly
版本142
开始支持,safari
完全不支持。
- 支持:主要在基于
功能不够丰富
没有饥饿处理机制:
react scheduler
可以为一个任务设置过期时间 (expirationTime
)。如果一个低优先级任务因为不断被高优先级任务插队而迟迟得不到执行,一旦它过期了,react scheduler
会将它提升到最高优先级,强制同步执行它,防止任务“饿死”。原生scheduler API
目前没有这个机制。优先级划分粒度大:
react scheduler
的优先级划分粒度为5
个bashImmediatePriority UserBlockingPriority LowPriority NormalPriority IdlePriority
而
postTask
的优先级划分粒度为3
个:bashuser-blocking user-visible background
因此
react scheduler
需要做一层适配,将5
个优先级映射为3
个优先级。jsswitch (priorityLevel) { case ImmediatePriority: case UserBlockingPriority: postTaskPriority = 'user-blocking'; break; case LowPriority: case NormalPriority: postTaskPriority = 'user-visible'; break; case IdlePriority: postTaskPriority = 'background'; break; default: postTaskPriority = 'user-visible'; break; }