对 Node.js 中的事件循环机制理解?

1. 事件循环机制 是什么
在 浏览器事件循环 中,我们了解到 JavaScript 在浏览器中的事件循环机制,其是根据 HTML5 定义的规范来实现
而在 Node.js 中, 事件循环 是基于 libuv 实现, libuv 是一个多平台的专注于异步 I/O 的库,如图:

上图 EVENT_QUEUE 给人看起来只有一个队列,但是实际上, EventLoop 存在 6 个阶段,每个阶段都有对应的一个先进先出的回调队列
2. 事件循环机制 执行过程
事件循环分成了 6 个阶段,对应如下:

timers:定时器检测阶段 这个阶段执行timer的回调,即setTimeout、setInterval的回调I/O callbacks:I/O 事件回调阶段 执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的I/O回调idle,prepare:闲置阶段 仅系统内部使用poll:轮询阶段 检查新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和setImmediate()调度的之外),其余情况node将在适当的时候在此阻塞check:检测阶段setImmediate()回调函数在这里执行close callbacks:关闭回调阶段 一些关闭的回调函数,如:socket.on('close', ...)
每个阶段对应一个队列,当事件循环进入某个阶段时,将会在该阶段内容执行回调,知道队列好紧或者回调的最大数量执行完毕,然后将进入下一个处理阶段
除了上述 6 个阶段,事件循环还有一个特殊的地方,就是 nextTick 队列,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡,即本阶段执行结束,进入下一个阶段前,所要执行的回调,类似插队
流程图如下所示:

在 Node 中,同样存在宏任务和微任务,与浏览器中的事件循环相似:
微任务对应有:
next tick queue:process.nextTickother queue:Promise的then回调、async/await、queueMicrotask等
宏任务对应有:
timers queue:setTimeout、setIntervalpoll queue:I/O事件check queue:setImmediateclose queue:close事件
其执行顺序为:
- next tick microtask queue
- other microtask queue
- timer queue
- poll queue
- check queue
- close queue
3. 事件循环机制 相关题目
下面代码的执行顺序是什么?
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout0");
}, 0);
setTimeout(function () {
console.log("setTimeout2");
}, 300);
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick1"));
async1();
process.nextTick(() => console.log("nextTick2"));
new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise2");
}).then(function () {
console.log("promise3");
});
console.log("script end");分析过程:
- 先找到同步任务,输出
script start - 遇到第一个
setTimeout,将其放入timer queue中 - 遇到第二个
setTimeout,300ms 后将其放入timer queue中 - 遇到
setImmediate,将其放入check queue中 - 遇到第一个
process.nextTick,将其放入next tick microtask queue中 - 执行
async1函数,输出async1 start,遇到await,将其后面的代码放入other microtask queue中 - 执行
async2函数,输出async2,async2后面的输出async1 end进入微任务,等待下一轮的事件循环 - 遇到第二个
process.nextTick,将其放入next tick microtask queue中 - 遇到
new Promise,输出promise1,promise2,将其放入other microtask queue中 - 遇到同步任务
console.log("script end"),输出script end - 本轮事件循环结束,进入下一轮事件循环,先依次输出
nextTick,分别是nextTick1、promise2 - 然后执行
other microtask queue,输出promise3、async1 end - 然后执行
timer queue,输出setTimeout0 - 然后执行
check queue,输出setImmediate - 300ms 后,执行
timer queue,输出setTimeout2
执行结果如下:
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2setTimeout 和 setImmediate 的执行顺序
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});输出情况如下:
情况一:
jssetTimeout; setImmediate;情况二:
jssetImmediate; setTimeout;
分析过程:
- 外层宏任务执行完毕,遇到异步 API 任务,将其放入对应的队列中
- 遇到
setTimeout,虽然设置的是 0 ms 触发,但实际上会被强制改成 1 ms,时间到了然后将其放入timer queue中 - 遇到
setImmediate,将其放入check queue中 - 同步代码执行完毕,进入下一轮事件循环
- 先进入
timer queue,检查当前时间是否到达setTimeout的时间,如果到达则执行,否则继续等待 - 再进入
check queue,执行setImmediate
这里的关键在于 1ms ,如果同步代码执行时间较长,进入 Event Loop 的时候 1ms 已经过了, setTimeout 已经被放入 timer queue 中,因此会先执行 setTimeout ,否则会先执行 setImmediate