执行原理
-
当 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 任务插入为止。
相关 API
// 检查阶段执行的异步函数
setImmediate()
// 当前 Event Loop 阶段执行完毕后,下个 Event Loop 阶段执行之前执行的异步函数
// 从技术实现上来说,它不是事件循环的一部分
process.nextTick()
// V8 引擎语言层面实现的一种微任务函数,也不是事件循环的一部分
Promise
setImmediate() 与 setTimeout()
两者的执行顺序,根据调用它们的上下文而有所不同。
情况1:不存在全局同步代码,直接在主模块执行时
// test.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// 执行顺序不固定
// 原因:受到进程性能的约束(这可能会受到机器上运行的其他应用的影响)
情况2:存在全局同步代码,直接在主模块执行时
// test.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
console.log('start');
// start -> timeout -> immediate
// 原因:先执行同步代码(2个异步函数已被加入各自队列),再开始 Event Loop,定时器阶段先于检查阶段,因此结果如上
情况3:.mjs代码,不存在全局同步代码,直接在主模块执行时
// test.mjs
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
// immediate -> timeout
// .mjs 本质上是一个 async 函数,因此实际代码等于:
async function main() {}
main().then(() => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
})
// 原因:
// 1. 先执行同步代码 main(),再开始 Event Loop。
// 2. 第一轮循环中,定时器阶段检查,未发现队列中有回调函数,然后执行微任务 then 回调。
// 3. 微任务回调执行过程中,将 setTimeout 和 setImmediate 加入各自队列。
// 4. 然后,第一轮循环继续进行,终于达到了检查阶段,发现了检查队列中存在 setImmediate,执行。
// 5. 最后,第一轮循环执行完毕,开始第二轮循环,定时器阶段检查,发现了上轮加入的 setTimeout,执行
情况4:.mjs代码,存在全局同步代码,直接在主模块执行时
// test.mjs
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
console.log('start');
// start -> immediate-> timeout
// .mjs 本质上是一个 async 函数,因此实际代码等于:
async function main() {
console.log('start');
}
main().then(() => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
})
情况5:执行 I/O 任务,在 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 先执行。
标签:回调,console,log,setTimeout,Nodejs,setImmediate,循环,执行,小记
From: https://www.cnblogs.com/kanyu/p/18350591