深入异步编程:事件循环(Event Loop)微任务与宏任务执行全景拆解
【摘要】 #深入异步编程:事件循环(Event Loop)微任务与宏任务执行全景拆解在实际开发中,许多开发者常因对 事件循环(Event Loop) 的执行顺序理解不够透彻,导致写出的异步代码出现难以排查的“诡异” Bug。本文将带你全景拆解 Node.js 事件循环的六个核心阶段,以及微任务(Microtask)与宏任务(Macrotask)的执行机制。 一、 什么是事件循环(Event Loop)...
#深入异步编程:事件循环(Event Loop)微任务与宏任务执行全景拆解
在实际开发中,许多开发者常因对 事件循环(Event Loop) 的执行顺序理解不够透彻,导致写出的异步代码出现难以排查的“诡异” Bug。
本文将带你全景拆解 Node.js 事件循环的六个核心阶段,以及微任务(Microtask)与宏任务(Macrotask)的执行机制。
一、 什么是事件循环(Event Loop)?
Node.js 是单线程的,但它之所以能够处理高并发的非阻塞 I/O 操作,核心秘密就在于事件循环。
简单来说,事件循环是 libuv 提供的一个机制:当 Node.js 启动时,会初始化事件循环,然后开始循环遍历底层的各种队列。如果发现有到期的定时器、准备就绪的 I/O 事件、或者需要执行的回调,就会将其推入调用栈中执行。
二、 libuv 事件循环的六大核心阶段
事件循环并不是一次性走完的,而是一个不断循环的环(Loop)。每一轮循环包含以下 6 个主要阶段(按执行顺序排列):
+---------------------------------+
| timers | <-----------------+
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| pending callbacks | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| idle, prepare | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| poll | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| check | |
+---------------------------------+ |
| |
v |
+---------------------------------+ |
| close callbacks | ------------------+
+---------------------------------+
- Timers(定时器阶段): 执行由
setTimeout()和setInterval()设定的回调。 - Pending Callbacks(挂起的回调): 执行上一轮循环中延迟到下一轮循环调度的某些系统级回调(如 TCP 错误类型)。
- Idle, Prepare(空闲与准备阶段): 仅供 libuv 内部使用,开发者无需关注。
- Poll(轮询阶段): 最核心的阶段。主要做两件事:
- 处理 I/O 事件(如读取文件、接收网络请求等)。
- 检查是否有到期的
timers,如果有,则会视情况跳转回timers阶段。
- Check(检查阶段): 专门用来执行
setImmediate()的回调。 - Close Callbacks(关闭回调阶段): 执行一些关闭事件的回调,例如
socket.on('close', ...)。
三、 微任务(Microtask)与宏任务(Macrotask)
在每个阶段之间,或者说在每个回调函数执行完毕后,事件循环会清空微任务队列。
- 宏任务(Macrotask / Tasks): 整个脚本代码(Script)、
setTimeout、setInterval、setImmediate、I/O 操作等。 - 微任务(Microtask):
- Node.js 特有的:
process.nextTick(优先级最高) - 标准 JavaScript 的:
Promise.resolve().then()、queueMicrotask
执行优先级原则
- 微任务优先于宏任务。
- 在所有微任务中,
process.nextTick的优先级高于普通的Promise微任务。 - 每当一个宏任务(或阶段中的回调)执行完后,引擎会立即检查并清空当前的微任务队列。
四、 经典代码剖析:你能全答对吗?
我们来看一段非常经典的异步执行顺序测试代码:
console.log('1. 同步代码开始');
setTimeout(() => {
console.log('2. setTimeout 宏任务');
Promise.resolve().then(() => {
console.log('3. setTimeout 内部的 Promise 微任务');
});
}, 0);
setImmediate(() => {
console.log('4. setImmediate 检查阶段');
});
process.nextTick(() => {
console.log('5. process.nextTick 微任务');
});
Promise.resolve().then(() => {
console.log('6. Promise 微任务');
});
console.log('7. 同步代码结束');
执行结果揭晓:
1. 同步代码开始
7. 同步代码结束
5. process.nextTick 微任务
6. Promise 微任务
2. setTimeout 宏任务
3. setTimeout 内部的 Promise 微任务
4. setImmediate 检查阶段
(注:setTimeout 和 setImmediate 在主模块顶层大范围调用时的执行顺序有时会受系统性能和时间戳抖动影响,但在典型场景下通常表现为 setTimeout 先于 setImmediate,或者受事件循环初始化的微小耗时影响。)
五、 总结
掌握事件循环的本质,不在于死记硬背阶段顺序,而在于理解:
- 同步任务最先执行。
- 微任务插队在任何宏任务及阶段转换的空隙中执行(尤其是
process.nextTick会在当前操作结束后立即执行)。 - 宏任务分布在
libuv的不同阶段中按部就班地运行。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)