Nodejs事件循环小记
执行原理
-
当 Node.js 启动时,会先初始化 Event Loop,然后执行提供的输入脚本(主模块同步代码),过程中可能会产生异步 API 调用、定时器或调用 process.nextTick(),然后开始处理事件循环。
-
Node.js 的 Event Loop 分为 6 个阶段,会按照顺序反复执行,每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。
-
每执行完一个阶段里的回调队列中的一个宏任务,就会去执行 process.nextTick 和 Promise 2个微任务队列,然后进入下一个阶段,这就是 Node.js Event Loop 的过程。
-
其中,process.nextTick 优先级高于 Promise,但是如果当前执行的是 Promise,那么如果执行过程中产生了 process.nextTick 和 Promise,那么后续的 Promise 会先于 process.nextTick 执行,直到 Promise 微任务队列清空。
阶段概述
-
定时器:此阶段执行由 setTimeout() 和 setInterval() 安排的回调。
-
待处理回调:执行被推迟到下一次循环迭代的 I/O 回调。
-
空闲,准备:仅在内部使用。
-
轮询:检索新的 I/O 事件,执行 I/O 相关回调(几乎是除了关闭回调、由定时器回调和 setImmediate 回调的所有回调)。
-
检查:setImmediate() 回调在此处调用。
-
关闭回调:一些关闭回调,例如 socket.on('close', ...)。
需要关注的主要是定时器、轮询、检查和关闭回调,重点是轮询。
-
当没有任何 I/O 任务时,事件循环会在轮询阶段等待,进入休眠期,直到新的 I/O 任务插入为止。
-
当存在 I/O 任务时,Event Loop 会在清空轮询队列后,检查 setImmediate队列 和 到期定时器,如果存在,就会结束当前轮询阶段,进入下一个阶段,最终执行它们。
相关 API
// 检查阶段执行的异步函数
setImmediate()
// 当前 Event Loop 阶段执行完毕后,下个 Event Loop 阶段执行之前执行的异步函数
// 从技术实现上来说,它不是事件循环的一部分
process.nextTick()
// V8 引擎语言层面实现的一种微任务函数,也不是事件循环的一部分
Promise
setImmediate() 与 setTimeout()
两者的执行顺序,根据调用它们的上下文而有所不同。
情况1:.js代码
// test.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// 执行顺序不固定
// 原因:取决于系统调度(基于机器性能和其他应用抢占等)
情况2:.js代码
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
for (let i = 0; i < 1000000; i++) {
// noop
}
// 执行顺序:timeout -> immediate
// 原因:同步代码的执行时间,超过了定时器时间,使得第 1 轮事件循环时,定时器阶段能够拿到队列回调
情况3:.mjs代码
// test.mjs
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// 执行顺序: immediate -> timeout 情况居多
// 原因:全局同步代码和微任务的执行,未占用对定时器的计时,使得第 1 轮事件循环时,定时器回调大概率未加入队列
// .mjs 本质上是一个 async 函数,因此实际代码等于:
async function main() {}
main().then(() => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
})
情况4:在 I/O 回调中加入
// test.js 或 test.mjs
// 带全局代码 或 不带全局代码
const fs = require('node:fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 执行顺序:immediate -> timeout
// 原因:因为 I/O 回调时,肯定处于轮询阶段,那么下一个阶段一定是检查阶段,所以,一定是 setImmediate 先执行。