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 先执行。
posted @ 2024-08-09 15:41  戡玉  阅读(4)  评论(0编辑  收藏  举报