一、why?
在 Node.js 中 promise.then 的优先级往往低于 process.nextTick 的优先级,但是也不是绝对的。 在 ESM 中测试时候,问题发生了反转。
二、前置知识:Node.js 脚本执行的各个阶段
阶段 | 描述 |
---|---|
启动阶段 (Start) | - 执行全局模块的代码。 |
- 初始化全局变量和函数。 | |
事件循环阶段 | timer 阶段: 执行 setTimeout 或 setInterval 注册的回调函数。 |
pending callback 阶段: 执行上一轮事件循环遗留的被延时的 I/O 回调函数。 | |
idle prepare 阶段: 仅用于 Node.js 内部模块的使用。 | |
poll 轮询阶段: 执行异步 I/O 的回调函数;计算当前轮询阶段阻塞后续阶段的时间。 | |
check 阶段: 当 poll 阶段回调函数队列为空时,执行 setImmediate 回调函数。 |
|
close 阶段: 执行注册 close 事件的回调函数。 |
|
执行微任务 (Microtasks) | - 处理 process.nextTick 注册的回调函数。 |
- 处理 Promise 的回调函数。 |
|
清理阶段 (Cleanup) | - 执行一些清理工作,例如关闭文件描述符等。 |
退出阶段 (Exit) | - 执行 process.on('exit') 注册的回调函数。 |
三、执行脚本
从事件循环中,围绕其周围的任务,都会与微任务队列进行交互 nextTickQueue
和 Promise Queue
和 microstack
进行交互。
四、问题模拟
定义两个文件:
CommonJS
: index.cjsESM
: index.mjs
内容都是一样的:
ts
process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
- 执行
index.cjs
输出的顺序是:1 2
- 执行
index.mjs
输出的顺序是:2 1
五、为什么?
这是因为,当作为 ESM 运行时,脚本实际上已经处于微任务阶段。因此,从那里排队的新微任务将在""回调之前执行
nextTick
。
esm 使用 async/await 评估
在 Node 中,ESM 模块脚本实际上是通过 async/await 函数进行评估的:
ts
async run() {
// ...
try {
await this.module.evaluate(timeout, breakOnSigint);
} catch (e) {
// ...
}
}
因此,这意味着当我们的脚本运行时,我们已经进入微任务阶段,因此从那里排队的新微任务将首先执行。
cjs 在 poll 阶段
在 commonjs 代码中,依然是同步运行,代码运行处在事件循环的轮询 (poll)阶段
ts
setImmediate(() => {
let winner;
console.log("轮询的结果:");
queueMicrotask(() => {
if (!winner) console.log(winner = "microtask");
});
process.nextTick(() => {
if (!winner) console.log(winner = "nextTick");
});
});
// 轮询的结果:
// nextTick
在 setImmediate 中进行,处于检查阶段,在里面创建两个任务 queueMicrotask
和 process.nextTick
, 输出的结果是 nextTick
, 说明 process.nextTick
的优先级高。
六、结论
本文关注 esm 下 nextTick 与 promise.then 执行顺序的差异行为。在 ESM 中由于在 async/await 的上下文中 已经处于微任务的环境中 promise.then 直接加入微任务队列中,比 process.nextTick 更快执行。