Node中返回Promise的I/O操作回调队列归属及代码运行疑问
Node事件循环:Promise化I/O操作的队列优先级与代码行为解析
首先明确Node事件循环的队列优先级:微任务队列(Promise/process.nextTick)> 定时器队列(setTimeout/setInterval)> I/O队列(异步I/O回调)。
第一段代码分析
async function asyncFunc() { let sum = 0; for (let i = 0; i < 100000; i++) { sum += i; } } async function main() { let start = Date.now(); let interval = setInterval(() => { let time = Date.now() - start; console.log(time); }, 1000); while (true) { await asyncFunc(); } } main();
这段代码里,asyncFunc是完全同步的CPU密集任务——虽然它是async函数,但内部没有异步操作,await asyncFunc()本质上会把while(true)的下一轮循环逻辑推入微任务队列。由于while(true)会不断往微任务队列里添加新的任务,导致微任务队列永远无法清空,事件循环永远轮不到定时器队列,因此setInterval的回调永远没有执行机会,也就永远不会输出时间。
第二段代码的疑问与解析
代码与输出
import fs from "fs-extra"; async function asyncFunc(j) { let sum = 0; console.log("checkpoint 1"); await fs.copy("file.txt", `file${j}.txt`, { overwrite: false }); for (let i = 0; i < 1000000000; i++) { sum += i; } console.log("checkpoint 2"); } async function main() { let start = Date.now(); let interval = setInterval(() => { let time = Date.now() - start; console.log(time); }, 1000); while (true) { const promises = []; for (let j = 0; j < 4; j++) { promises.push(asyncFunc(j)); } await Promise.all(promises); } } main();
输出:
checkpoint 1 checkpoint 1 checkpoint 1 checkpoint 1 checkpoint 2 checkpoint 2 checkpoint 2 2123 checkpoint 2
核心疑问解答
你观察到的现象确实和竞态条件有关,但核心需要明确Promise化I/O操作的队列逻辑:
Node中,像fs-extra.copy这类返回Promise的I/O操作,底层流程是:
- 发起异步I/O请求后,I/O完成的回调会被推入I/O队列(Poll阶段);
- 当事件循环处理到I/O队列的该回调时,会执行
Promise.resolve(),进而把await之后的代码(也就是那个CPU密集的大循环和checkpoint 2输出逻辑)推入微任务队列。
现在拆解这段代码的执行流程:
main函数启动后,setInterval注册了1秒后触发的定时器回调,随后进入while(true)循环,创建4个asyncFunc实例:- 每个
asyncFunc先输出checkpoint 1,然后调用fs.copy发起异步文件复制,await会暂停当前函数,返回一个pending状态的Promise。
- 每个
- 4个
checkpoint 1输出完成后,主线程同步代码执行完毕,进入事件循环的Poll阶段,等待I/O事件完成。 - 由于文件是空的,复制操作耗时极短,但四个文件的I/O完成时间存在细微差异:
- 前三个文件的复制先完成,对应的I/O回调被Poll阶段处理,触发Promise resolve,三个
await后的CPU密集任务被推入微任务队列; - 微任务队列被依次处理:三个大循环执行完毕,输出三个
checkpoint 2,此时已经过了约1.5秒,定时器的触发时间已到,回调进入定时器队列。 - 微任务队列清空后,事件循环进入Timers阶段,处理定时器回调,输出
2123。 - 随后回到Poll阶段,处理第四个文件的I/O回调,触发对应的Promise resolve,第四个CPU密集任务进入微任务队列并执行,输出最后一个
checkpoint 2。
- 前三个文件的复制先完成,对应的I/O回调被Poll阶段处理,触发Promise resolve,三个
这就是为什么定时器输出出现在三个checkpoint 2之后、第四个之前——本质是四个文件I/O完成的微小时间差导致的竞态,而非队列优先级的逻辑错误。
结论
- 返回Promise的I/O操作,底层I/O完成回调进入I/O队列,而Promise resolve后的后续逻辑(
await或.then的回调)进入微任务队列; - 你的第二段代码现象是I/O完成时间的细微差异引发的竞态条件,符合Node事件循环的优先级规则。
内容的提问来源于stack exchange,提问作者J Booth
相关产品推荐
相关产品推荐

