(2)事件循环的流程
其中libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。下面 是Eventloop 事件循环的流程:
整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。下面来看下这六个阶段都做了哪些事:
timers
阶段:执行timer(setTimeout、setInterval)的回调,由 poll 阶段控制;I/O callbacks
阶段:主要执行系统级别的回调函数,比如 TCP 连接失败的回调;idle, prepare
阶段:仅Node.js内部使用,可以忽略;poll
阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等;check
阶段:执行 setImmediate() 的回调;close callbacks
阶段:执行关闭请求的回调函数,比如socket.on('close', ...)
注意:上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段,这里也是与浏览器中逻辑差异较大的地方。
其中,这里面比较重要的就是第四阶段:poll,这一阶段中,系统主要做两件事:
- 回到 timer 阶段执行回调
- 执行 I/O 回调
在进入该阶段时如果没有设定了 timer 的话,会出现以下情况:
(1)如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制;
(2)如果 poll 队列为空时,会出现以下情况:
- 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调;
- 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去;
当设定了 timer 且 poll 队列为空,则会判断是否有 timer 超时,如果有的就会回到 timer 阶段执行回调。
这一过程的具体执行流程如下图所示:
(3)宏任务和微任务
Node.js事件循环的异步队列也分为两种:宏任务队列和微任务队列。
- 常见的宏任务:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。
- 常见的微任务:process.nextTick、new Promise().then(回调)等。
(4)process.nextTick()
上面提到了process.nextTick(),它是node中新引入的一个任务队列,它会在上述各个阶段结束时,在进入下一个阶段之前立即执行。
Node.js官方文档的解释如下:
process.nextTick()is not technically part of the event loop. Instead, thenextTickQueuewill be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.
例如下面的代码:
setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.error('promise') }) process.nextTick(() => { console.error('nextTick') }) 复制代码
输出结果如下:
nextTick promise timeout 复制代码
可以看到,process.nextTick()是优先于promise的回调执行。
(5)setImmediate 和 setTimeout
上面还提到了setImmediate 和 setTimeout,这两者很相似,主要区别在于调用时机的不同:
- setImmediate:在poll阶段完成时执行,即check阶段;
- setTimeout:在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行;
例如下面的代码:
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); 复制代码
输出结果如下:
timeout setImmediate 复制代码
在上面代码的执行过程中,第一轮循环后,分别将 setTimeout 和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入timers 阶段,执行定时器队列回调,然后 pending callbacks和poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为timeout、setImmediate。
4. Node与浏览器Event Loop差异
Node.js与浏览器的 Event Loop 差异如下:
- Node.js:microtask 在事件循环的各个阶段之间执行;
- 浏览器:microtask 在事件循环的 macrotask 执行完之后执行;
- 执行全局的 Script 代码(与浏览器无差);
- 把微任务队列清空:注意,Node 清空微任务队列的手法比较特别。在浏览器中,我们只有一个微任务队列需要接受处理;但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务;
- 开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到系统限制);
- 步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环。