Node
一、认识
Node.js
的事件循环(Event Loop
)是其异步非阻塞 I/O
模型的核心机制,负责协调和调度所有的异步操作,从而使单线程的 Node.js
能高效地处理大量并发请求。Node.js
的事件循环基于libuv 引擎
一共有六个阶段, 每一个阶段都有一个 先入先出 FIFO
队列, 异步操作在后台线程中执行, 执行完成后, 将回调函数放入相应的队列等待事件循环处理, 事件循环不断检查不同阶段的任务队列,并依次执行队列中的回调,确保各个异步任务得到及时处理。Node.js
六个阶段分别为:
-
Timers
定时器阶段: 此阶段会检查是否有到期的定时器任务, 然后执行这些任务的回调。处理setTimeout()
和setInterval()
回调函数。Timers
定时器 的最小延迟时间不能小于1ms
, 即使设置为0
,实际也会被强制设置为1ms
。 -
I/O Callbacks
阶段: 处理上一轮循环延迟到下一轮的I/O
回调(例如: 网络请求、文件系统操作等)。此阶段会执行几乎所有I/O
操作的回调。 -
Idle, Prepare
阶段: 内部使用的阶段,主要用于处理一些预备工作,不直接暴露给开发者。 -
Poll
阶段: 当Poll
队列为空时, 会检查是否有setImmediate
回调, 如果有setImmediate
回调, 会直接进入Check
阶段; 如果没有setImmediate
回调, 会等待新的I/O
事件。 -
Check
阶段: 专门用于处理setImmediate()
的回调。这一阶段的任务队列会在poll
阶段结束后立即执行。 -
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
:Promise
的then
回调、queueMicrotask
3.3 任务执行顺序
-
next tick microtask queue
-
other microtask queue
-
timer queue
-
poll queue
-
check queue
-
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()
会先执行
-
外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
-
遇到
setTimeout
, 虽然设置的是0
毫秒触发, 但实际上会被强制改成1ms
,时间到了然后塞入times
阶段。注意: 实际上, 浏览器和Node.js
中存在一个最小定时器阈值(在浏览器中常见的最小值为4ms
,Node.js
通常也有类似机制), 但并不是说每次都严格等1ms
, 这个值受平台实现和调度延迟等因素影响 -
遇到
setImmediate
塞入check
阶段 -
同步代码执行完毕, 进入
Event Loop
-
先进入
times
阶段, 检查当前时间过去了1
毫秒没有, 如果过了1
毫秒, 满足setTimeout
条件, 执行回调, 如果没过1
毫秒, 跳过 -
跳过空的阶段, 进入
check
阶段, 执行setImmediate
回调 -
这里的关键在于这
1ms
,如果同步代码执行时间较长,进入Event Loop
的时候1
毫秒已经过了,setTimeout
先执行,如果1
毫秒还没到,就先执行了setImmediate