渊虹小站

Nodejs事件循环

总览

node事件循环的各个阶段,如下图所示:

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

总体执行流程

node代码执行的流程:

在你通过node执行一个脚本时,比如node my-scripts.js

  • 在单线程作用下,node会先执行所有同步的主代码。
  • 在所有同步代码执行完毕后,node会查看事件循环是否存活,在事件循环中是否有待执行内容。

    • 若事件循环中无待执行内容,则尝试执行退出回调,比如process.on('exit',foo),执行完毕后退出。
    • 若事件循环中有带执行内容,则进入事件循环,按照上图所示的各个阶段依次执行。
  • 一个循环执行完毕后,会再次检测循环是否存活,是否有待执行内容,若有,则开启新的一轮循环;若无,则程序执行退出操作,进行退出。

以服务器代码为例,开启服务器,则会先执行所有同步的主流程代码,然后进入事件循环,其他非请求事件执行完毕后,服务器进入到监听请求状态,事件循环会不断进行,监听请求,请求进入后,事件循环执行I/O回调,完毕后再次进入监听状态。


各阶段详情

timers 阶段

通过setTimeoutsetInterval指定的内容都将在这个阶段被执行。当循环进行到这个阶段时,时间到期的定时回调会被执行。

在Timers阶段中,各个定时器会按照距离需要执行的时间从近到远的顺序,被放在一个堆内存中。当事件循环执行到这个阶段时,会依次获取堆中的定时器,查看定时器是否到期,若到期,则执行定时器回调,执行后查看下一个,若下一个也到期,则继续执行,直到:

  • 遇到第一个未到期的定时器,则停止执行,进入事件循环下一个阶段。因为定时器按照距离需要执行事件从近到远的顺序排列,所以遇到第一个未到期的定时器,则表示后面的定时器都是未到期的。
  • 或者事件循环在这个阶段的执行事件超过系统限定的此阶段的最大时长,此时也需要停止此阶段的处理,进入下一阶段。
pending 阶段

这个阶段会执行被挂起的 I/O 回调。大多数的 I/O 回调都会在轮询到 I/O 结束时直接立马被调用(在poll阶段中),但是有一些回调会被推迟到下一个循环,被上一个循环推迟的 I/O 回调会在这个阶段被执行。

当事件循环进入到这个阶段时,会依次执行上一个循环中被推迟的I/O回调,直到队列总的所有回调都被执行完,或者总执行时长达到系统限定的此阶段执行时间。

Idle,Prepare 阶段

libUV 内部使用

poll 阶段

轮询阶段执行的内容是:检索新的I/O事件,执行I/O回调。除关闭函数、计时器回调及 setImmediate()等指定的回调外的其他回调,几乎都是在这个阶段被执行的。事件循环进入到这个阶段后:

  • 若此轮询执行队列中已经有内容,则会直接处理队列中的内容,直到队列为空或者执行时间达到系统指定最长时限
  • 如果轮询队列中没有内容,node会进行等待,等待I/O回调被添加到轮询执行队列中,若有回调被添加,则立即执行。

如上所说到的,轮询队列可能会进行等待,等待的时间受各种因素影响:

  • 如果事件循环以UV_RUN_NOWAIT形式进行,则不进行等待,直接进入下一阶段
  • 如果事件循环需要停止,则不进行等待
  • 如果没有活跃的处理或请求,也就相当于没有后续需要等待被处理的I/O回调,则不进行等待
  • 如果idle阶段有活跃的内容待处理,则不等待
  • 如果关闭阶段有内容在等待被处理,则不等待
  • 如果上面情况都没有出现,则查看timer阶段中的堆内存,取第一个计时器的,等待时间根据距离这个定时器执行需要的时间而定。如果此时没有活跃的定时器,则等待时间为无穷大。(等待时间为无穷大,但是整个阶段受系统指定此阶段处理最大时间限制,到达这个限制时间还是会进入下一个阶段)
check 阶段

执行通过setImmediate()注册的回调。事件循环到这个阶段时,会依次同步地执行setImmediate回调,直到所有回调被执行完或者达到系统限定的此阶段执行时间。

close 阶段

执行close事件的回调,比如socket.on('close', …)


nextTickQueue 和 microTaskQueue

nextTickQueuemicroTaskQueue是另外两个与node事件循环有关的队列。nextTickQueue中存放调用process.nextTick()指定的回调。microTaskQueue中存放宏任务回调,比如resolved的Promise回调。他们不是在libUV库中原生实现的,而是在node.js上层中实现的。

这两个队列中的任务会被尽快执行,其执行时间,是事件循环从一个阶段进入下一个阶段的间隙。和事件循环的各个阶段不同,这两个队列的执行没有系统时间限制,node会执行它们,直到队列为空。

nextTickQueue 相对 microTaskQueue 拥有更高的优先级。(需要注意的是,如果使用的是V8提供的原生Promise,nextTickQueue的优先级高于作为宏任务回调的Promise回调,但是如果使用的是q或者bluebird这类JS库提供的Promise,情况则不一样了。)


参考文章:

nodejs事件循环、计时器和process.nextTick()

nodejs事件循环的流程和生命周期

nodejs事件循环概览

libuv的I/O循环