跳到主要内容

Node

2024年04月07日
柏拉文
越努力,越幸运

一、认识


Node.js 的事件循环(Event Loop)是其异步非阻塞 I/O 模型的核心机制,负责协调和调度所有的异步操作,从而使单线程的 Node.js 能高效地处理大量并发请求。Node.js 的事件循环基于libuv 引擎 一共有六个阶段, 每一个阶段都有一个 先入先出 FIFO 队列, 异步操作在后台线程中执行, 执行完成后, 将回调函数放入相应的队列等待事件循环处理, 事件循环不断检查不同阶段的任务队列,并依次执行队列中的回调,确保各个异步任务得到及时处理。Node.js 六个阶段分别为:

  1. Timers 定时器阶段: 此阶段会检查是否有到期的定时器任务, 然后执行这些任务的回调。处理 setTimeout()setInterval() 回调函数。Timers 定时器 的最小延迟时间不能小于 1ms, 即使设置为 0,实际也会被强制设置为 1ms

  2. I/O Callbacks 阶段: 处理上一轮循环延迟到下一轮的 I/O 回调(例如: 网络请求、文件系统操作等)。此阶段会执行几乎所有 I/O 操作的回调。

  3. Idle, Prepare 阶段: 内部使用的阶段,主要用于处理一些预备工作,不直接暴露给开发者。

  4. Poll 阶段: 当 Poll 队列为空时, 会检查是否有 setImmediate 回调, 如果有 setImmediate 回调, 会直接进入 Check 阶段; 如果没有 setImmediate 回调, 会等待新的 I/O 事件。

  5. Check 阶段: 专门用于处理 setImmediate() 的回调。这一阶段的任务队列会在 poll 阶段结束后立即执行。

  6. Close Callbacks 阶段: 处理所有关闭事件的回调,例如 socket.on('close', ...)

Node.js 版本之间事件循环机制差异:

Node.js 11 之前: 同一个阶段全部MacroTask(宏任务)执行完毕完毕,才会执行MicroTask(微任务)

Node.js 11 之后: 同一个阶段只要执行了MacroTask(宏任务),就会立即执行MicroTask(微任务),与浏览器表现一致

三、任务


3.1 Macro-Task (宏任务)

  • I/O

  • UI 交互事件

  • setTimeout

  • setInterval

  • setImmediate

  • MessageChannel

3.2 Micro-Task (微任务)

  • next tick queue: process.nextTick 不属于事件循环的任何阶段, 它类似于一个微任务, 它的优先级高于所有微任务(包括 Promise

  • other queue: Promisethen 回调、queueMicrotask

3.3 任务执行顺序

  1. next tick microtask queue

  2. other microtask queue

  3. timer queue

  4. poll queue

  5. check queue

  6. close queue

四、事件循环循序


4.1

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
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2

先找到同步任务,输出script start

遇到第一个 setTimeout,将里面的回调函数放到 timer 队列中

遇到第二个 setTimeout,300ms后将里面的回调函数放到 timer 队列中

遇到第一个setImmediate,将里面的回调函数放到 check 队列中

遇到第一个 nextTick,将其里面的回调函数放到本轮同步任务执行完毕后执行

执行 async1函数,输出 async1 start

执行 async2 函数,输出 async2,async2 后面的输出 async1 end进入微任务,等待下一轮的事件循环

遇到第二个,将其里面的回调函数放到本轮同步任务执行完毕后执行

遇到 new Promise,执行里面的立即执行函数,输出 promise1、promise2

then里面的回调函数进入微任务队列

遇到同步任务,输出 script end

执行下一轮回到函数,先依次输出 nextTick 的函数,分别是 nextTick1、nextTick2

然后执行微任务队列,依次输出 async1 end、promise3

执行timer 队列,依次输出 setTimeout0

接着执行 check 队列,依次输出 setImmediate

300ms后,timer 队列存在任务,执行输出 setTimeout2

4.2 关于 setTimeout 与 setImmediate 的输出顺序

setTimeout(() => {
console.log("setTimeout");
}, 0);

setImmediate(() => {
console.log("setImmediate");
});

// 情况一:
setTimeout
setImmediate

// 情况二:
setImmediate
setTimeout

setImmediate()setTimeout() 的执行顺序取决于当前事件循环的状态,一般 setImmediate() 会先执行

  1. 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段

  2. 遇到 setTimeout, 虽然设置的是 0 毫秒触发, 但实际上会被强制改成 1ms,时间到了然后塞入 times 阶段。注意: 实际上, 浏览器和 Node.js 中存在一个最小定时器阈值(在浏览器中常见的最小值为 4ms, Node.js 通常也有类似机制), 但并不是说每次都严格等 1ms, 这个值受平台实现和调度延迟等因素影响

  3. 遇到 setImmediate 塞入 check 阶段

  4. 同步代码执行完毕, 进入 Event Loop

  5. 先进入 times 阶段, 检查当前时间过去了 1 毫秒没有, 如果过了 1 毫秒, 满足 setTimeout 条件, 执行回调, 如果没过 1 毫秒, 跳过

  6. 跳过空的阶段, 进入check阶段, 执行setImmediate回调

  7. 这里的关键在于这 1ms,如果同步代码执行时间较长,进入 Event Loop 的时候 1 毫秒已经过了, setTimeout 先执行,如果 1 毫秒还没到,就先执行了setImmediate

参考资料


Node.js 事件循环机制

「进击的前端工程师」一文带你搞懂JavaScript事件循环

又被node的eventloop坑了,这次是node的锅

一次弄懂Event Loop

「前端进阶」从多线程到Event Loop全面梳理

深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)