Node事件循环(Event Loop)机制探索

850 阅读4分钟

前言

前几天看到很有意思的一篇文章,是说node10和node11对同一段代码输出有所区别,之前我也有写过一篇关于JS的运行机制的文章,因此这边也对Node的运行机制进行总结整理,看看两者到底有什么区别。

有意思的例子

setTimeout(() => {
  console.log('timer1');
  Promise.resolve().then(function() {
    console.log('promise1');
  });
}, 0);
setTimeout(() => {
  console.log('timer2');
  Promise.resolve().then(function() {
    console.log('promise2');
  });
}, 0);

看到这个例子,根据我对JS时间循环的了解,首先执行第一个计时器,顺便执行promise.then这个微任务,然后再去执行第二个计时器,所以输出应该是timer1 promise1 timer2 promise2。

但是有意思的是node10的输出结果却是timer1 timer2 promise1 promise2,至于为什么会这么输出,我们需要去了解node中的事件循环。

Node的事件循环机制

和JavaScript类似,Node.js 在主线程里维护了一个**事件队列,**当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。

而当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:

  如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;

  如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。而 I/O 处理方面Node使用了Libuv,Libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。

先贴一张官方的图

img

忽略上面上面判断事件循环是否还存在,我们可以认为,整个事件循环大致有六个阶段

timers:这个阶段执行定时器队列中的回调,如 setTimeout()setInterval()

I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。

idle, prepare: 这个阶段仅在内部使用,可以不必理会。

poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。

check: setImmediate()的回调会在这个阶段执行。

close callbacks: 例如socket.on('close', ...)这种close事件的回调。

用流程图来表达可能更容易理解

node中的宏任务和微任务

浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。

img

这样我们就能理解为什么Node 端运行结果为timer1 timer2 promise1 promise2

主代码执行,将 2 个 定时器 依次放入 I/O任务队列最后进入timer队列,主代码执行完毕,调用栈空闲,开始进行事件循环首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2。

而node11之后,node在setTimeOut执行后会手动清空微任务队列,以保证结果贴近浏览器。

写在最后

算是填了之前在写JS的事件循环时的坑,同时在学习node事件循环的时候,了解到在每一轮判断中都有观察者,用于判断是否有事件要处理。算是解决了我之前的一些疑惑,在这里也一并记录。

参考

深入理解js事件循环机制(Node.js篇)