Node.js事件循环在阻塞代码下的行为差异及微任务优先级疑问
Node.js事件循环在阻塞代码下的行为差异及微任务优先级疑问
嘿,这个问题问得特别到位,正好戳中了Node.js事件循环里容易被忽略的「阶段顺序+异步任务调度时机」细节!我来给你一步步拆解清楚~
先还原你的问题场景
你的测试代码
const fs = require('node:fs/promises'); // Schedule a timeout setTimeout(() => { console.log("set timeout callback is called!"); }, 10); function consumeCpu(limit) { console.log(`waiting a while (limit is ${limit})`); let sum = 1; for (let i = 0; i < limit; i++) { sum += i; } } let cycles = Number(process.argv[2]); // Wait a while (blocking CPU) consumeCpu(cycles); // Run an async I/O action which will fail (async () => { try { // This file does not exist, so an error will be thrown let data = await fs.readFile('/tmp/123.txt'); } catch (err) { console.log("err is: " + err); } })(); // Wait a while again (blocking CPU) consumeCpu(cycles); console.log("end of script"); // Let event loop start processing
两种运行结果
短阻塞(node test.js 100)输出:
waiting a while (limit is 100) waiting a while (limit is 100) end of script err is: Error: ENOENT: no such file or directory, open '/tmp/123.txt' set timeout callback is called!
长阻塞(node test.js 1000000000)输出:
waiting a while (limit is 1000000000) waiting a while (limit is 1000000000) end of script set timeout callback is called! err is: Error: ENOENT: no such file or directory, open '/tmp/123.txt'
核心原因:事件循环阶段顺序 + 异步任务的调度时机
你之前的认知「微任务优先于宏任务」没错,但这里的关键是:微任务的创建本身依赖于前面的宏任务(I/O回调)被执行,而长阻塞场景下,定时器的宏任务已经先进入了事件循环的优先处理阶段。
我给你拆解两种场景的执行流程:
1. 短阻塞场景(阻塞时间<10ms)
- 同步代码先跑完:两次
consumeCpu(100)→console.log("end of script")。此时setTimeout的10ms超时时间还没到,定时器回调还没被加入事件循环的timers阶段队列。 - 事件循环启动,首先进入
timers阶段:没有可执行的回调,直接跳过。 - 进入
poll阶段(处理I/O回调的核心阶段):此时fs.readFile的I/O操作已经完成(因为操作系统后台处理I/O和Node.js的CPU阻塞是并行的),它的错误回调被执行,触发了promise的reject,进而把catch里的代码加入微任务队列。 - 处理微任务队列:执行
catch的console.log,输出err is: ...。 - 事件循环继续循环,再次进入
timers阶段:此时setTimeout已经超时,执行它的回调,输出set timeout callback is called!。
2. 长阻塞场景(阻塞时间>>10ms)
- 同步代码执行期间:因为阻塞时间远超过10ms,
setTimeout的超时时间已到,它的回调被加入事件循环的timers阶段队列。同时,fs.readFile的I/O操作也已经完成,它的错误回调被加入poll阶段队列。 - 同步代码跑完后,事件循环启动,首先进入
timers阶段:找到已经超时的setTimeout回调,直接执行,输出set timeout callback is called!。此时微任务队列是空的(因为fs.readFile的回调还没被执行,promise还没被reject,自然没有相关微任务)。 - 事件循环继续推进,依次跳过
pending callbacks、idle/prepare阶段,进入poll阶段:执行fs.readFile的错误回调,触发promise reject,把catch代码加入微任务队列。 - 处理微任务队列:执行
catch的console.log,输出err is: ...。
总结
你的误区是默认「promise的微任务已经存在」,但实际上fs.promises.readFile对应的微任务(catch里的逻辑),必须等Node.js的事件循环处理完I/O回调、触发promise reject后才会被创建。而长阻塞场景下,定时器的宏任务已经先进入了事件循环的第一个处理阶段(timers),自然会先被执行。
内容来源于stack exchange
相关产品推荐
相关产品推荐

