Node中如何实现事件循环

这篇文章主要介绍Node中如何实现事件循环,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

Node.js 做为 JavaScript 的服务端运行时,主要与网络、文件打交道,没有了浏览器中事件循环的渲染阶段。

在浏览器中有 HTML 规范来定义事件循环的处理模型,之后由各浏览器厂商实现。Node.js 中事件循环的定义与实现均来自于 Libuv。

Libuv 围绕事件驱动的异步 I/O 模型而设计,最初是为 Node.js 编写的,提供了一个跨平台的支持库。下图展示了它的组成部分,Network I/O 是网络处理相关的部分,右侧还有文件操作、DNS,底部 epoll、kqueue、event ports、IOCP 这些是底层不同操作系统的实现。

Node中如何实现事件循环

事件循环的六个阶段

当 Node.js 启动时,它会初始化事件循环,处理提供的脚本,同步代码入栈直接执行,异步任务(网络请求、文件操作、定时器等)在调用 API 传递回调函数后会把操作转移到后台由系统内核处理。目前大多数内核都是多线程的,当其中一个操作完成时,内核通知 Node.js 将回调函数添加到轮询队列中等待时机执行。

下图左侧是 Node.js 官网对事件循环过程的描述,右侧是 Libuv 官网对 Node.js 的描述,都是对事件循环的介绍,不是所有人上来都能去看源码的,这两个文档通常也是对事件循环更直接的学习参考文档,在 Node.js 官网介绍的也还是挺详细的,可以做为一个参考资料学习。

Node中如何实现事件循环

左侧 Node.js 官网展示的事件循环分为 6 个阶段,每个阶段都有一个 FIFO(先进先出)队列执行回调函数,这几个阶段之间执行的优先级顺序还是明确的。

右侧更详细的描述了,在事件循环迭代前,先去判断循环是否处于活动状态(有等待的异步 I/O、定时器等),如果是活动状态开始迭代,否则循环将立即退出。

下面对每个阶段分别讨论。

timers(定时器阶段)

首先事件循环进入定时器阶段,该阶段包含两个 API setTimeout(cb, ms)、setInterval(cb, ms) 前一个是仅执行一次,后一个是重复执行。

这个阶段检查是否有到期的定时器函数,如果有则执行到期的定时器回调函数,和浏览器中的一样,定时器函数传入的延迟时间总比我们预期的要晚,它会受到操作系统或其它正在运行的回调函数的影响。

例如,下例我们设置了一个定时器函数,并预期在 1000 毫秒后执行。

const now = Date.now();
setTimeout(function timer1(){
  log(`delay ${Date.now() - now} ms`);
}, 1000);
setTimeout(function timer2(){
 log(`delay ${Date.now() - now} ms`);
}, 5000);
someOperation();

function someOperation() {
  // sync operation...
  while (Date.now() - now < 3000) {}
}

当调用 setTimeout 异步函数后,程序紧接着执行了 someOperation() 函数,中间有些耗时操作大约消耗 3000ms,当完成这些同步操作后,进入一次事件循环,首先检查定时器阶段是否有到期的任务,定时器的脚本是按照 delay 时间升序存储在堆内存中,首先取出超时时间最小的定时器函数做检查,如果 nowTime - timerTaskRegisterTime > delay 取出回调函数执行,否则继续检查,当检查到一个没有到期的定时器函数或达到系统依赖的最大数量限制后,转移到下一阶段。

在我们这个示例中,假设执行完 someOperation() 函数的当前时间为 T + 3000:

检查 timer1 函数,当前时间为 T + 3000 - T > 1000,已超过预期的延迟时间,取出回调函数执行,继续检查。

检查 timer2 函数,当前时间为 T + 3000 - T < 5000,还没达到预期的延迟时间,此时退出定时器阶段。

pending callbacks

定时器阶段完成后,事件循环进入到 pending callbacks 阶段,在这个阶段执行上一轮事件循环遗留的 I/O 回调。根据 Libuv 文档的描述:大多数情况下,在轮询 I/O 后立即调用所有 I/O 回调,但是,某些情况下,调用此类回调会推迟到下一次循环迭代。听完更像是上一个阶段的遗留。

Node中如何实现事件循环