前言
Javascript 在浏览器中的事件循环(Event Loop)机制,其是根据 HTML5 定义的规范来实现,而 Node 的事件循环基于异步 IO 库 libuv 实现。Node 启动时将会初始化并执行事件循环。
事件循环
Node 事件循环有以下 6 个阶段:
graph LR A(timers)-->B("pending callbacks")-->C("idle, prepare")-->D("poll")-->E("check")-->F("close callbacks")-->A
在 timers 阶段,Node 处理超时的setTimeout
和setInterval
。
pending callbacks 阶段执行某些系统操作的回调,例如 TCP 连接错误。
idle, prepare 阶段仅在 Node 内部使用。
poll 轮询阶段计算应该阻塞和轮询 I/O 的时间,然后处理轮询队列中的事件。当事件循环进入 poll 阶段并且没有计时器时,
-
如果轮询队列不为空,则事件循环将迭代其回调队列,同步执行它们,直到队列耗尽或达到系统相关的时间限制。
-
如果轮询队列为空,如果有
setImmediate
调度,结束本阶段,并且进入 check 阶段阶段执行调度的代码。否则,事件循环将等待回调被添加到队列中,然后立即执行。
如果轮询队列为空,将检查是否有计时器达到阈值。有则返回到 timer 阶段以执行计时器回调。
check 阶段执行setImmediate
的回调。该阶段意味着 poll 阶段的结束。
close callbacks 执行关闭的回调,例如socket.on('close', ...)
。
总结一下:
graph LR A(timers)-->B("pending callbacks")-->C("idle, prepare")-->D("poll")-->|达到限制|E("check")-->F("close callbacks")-->A D-->|"轮询队列为空 & 没有计时器 & setImmediate"|E D-->|"轮询队列为空 & 计时器"|A
微任务
上面的所说的回调,其实是宏任务(Macrotask)。Node 的微任务有 3 种:Promise.then
、process.nextTick
、queueMicrotask
。Node 11 之后,在每一个阶段的每一个宏任务执行前(之前是执行每一阶段前),Node 执行并清空微任务队列,也包括执行微任务时产生的微任务。
Promise.then
和process.nextTick
的任务属于不同的微任务队列。前者由 V8 引擎管理,后者由 Node 自身管理。Node 会先执行process.nextTick
的队列,再执行Promise.then
的。queueMicrotask
这个微任务和Promise.then
使用同一个微任务队列。
此外,Node 会先执行完当前微队列的任务,举个栗子:
ts- import process from "process";
- Promise.resolve().then(() => {
- console.log(2);
- process.nextTick(() => console.log(5))
- Promise.resolve().then(() => {
- console.log(3)
- Promise.resolve().then(() => {
- console.log(4)
- })
- })
- })
- process.nextTick(() => console.log(0))
- queueMicrotask(() => console.log(1))
这段代码结果是 0 1 2 3 4 5。process.nextTick
优先,则先打印 0。另外,可以发现process.nextTick
的 5 排在Promise.resolve().then
的 3 4 后面,这是因为 Node 执行某个任务队列时,会先执行完该队列任务。