node事件循环的各个阶段,如下图所示:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
node代码执行的流程:
在你通过node执行一个脚本时,比如node my-scripts.js
。
在所有同步代码执行完毕后,node会查看事件循环是否存活,在事件循环中是否有待执行内容。
process.on('exit',foo)
,执行完毕后退出。以服务器代码为例,开启服务器,则会先执行所有同步的主流程代码,然后进入事件循环,其他非请求事件执行完毕后,服务器进入到监听请求状态,事件循环会不断进行,监听请求,请求进入后,事件循环执行I/O回调,完毕后再次进入监听状态。
通过setTimeout
和setInterval
指定的内容都将在这个阶段被执行。当循环进行到这个阶段时,时间到期的定时回调会被执行。
在Timers阶段中,各个定时器会按照距离需要执行的时间从近到远的顺序,被放在一个堆内存中。当事件循环执行到这个阶段时,会依次获取堆中的定时器,查看定时器是否到期,若到期,则执行定时器回调,执行后查看下一个,若下一个也到期,则继续执行,直到:
这个阶段会执行被挂起的 I/O 回调。大多数的 I/O 回调都会在轮询到 I/O 结束时直接立马被调用(在poll阶段中),但是有一些回调会被推迟到下一个循环,被上一个循环推迟的 I/O 回调会在这个阶段被执行。
当事件循环进入到这个阶段时,会依次执行上一个循环中被推迟的I/O回调,直到队列总的所有回调都被执行完,或者总执行时长达到系统限定的此阶段执行时间。
libUV 内部使用
轮询阶段执行的内容是:检索新的I/O事件,执行I/O回调。除关闭函数、计时器回调及 setImmediate()
等指定的回调外的其他回调,几乎都是在这个阶段被执行的。事件循环进入到这个阶段后:
如上所说到的,轮询队列可能会进行等待,等待的时间受各种因素影响:
UV_RUN_NOWAIT
形式进行,则不进行等待,直接进入下一阶段执行通过setImmediate()
注册的回调。事件循环到这个阶段时,会依次同步地执行setImmediate
回调,直到所有回调被执行完或者达到系统限定的此阶段执行时间。
执行close事件的回调,比如socket.on('close', …)
。
nextTickQueue
和microTaskQueue
是另外两个与node事件循环有关的队列。nextTickQueue
中存放调用process.nextTick()
指定的回调。microTaskQueue
中存放宏任务回调,比如resolved的Promise回调。他们不是在libUV
库中原生实现的,而是在node.js
上层中实现的。
这两个队列中的任务会被尽快执行,其执行时间,是事件循环从一个阶段进入下一个阶段的间隙。和事件循环的各个阶段不同,这两个队列的执行没有系统时间限制,node会执行它们,直到队列为空。
nextTickQueue
相对 microTaskQueue
拥有更高的优先级。(需要注意的是,如果使用的是V8提供的原生Promise,nextTickQueue的优先级高于作为宏任务回调的Promise回调,但是如果使用的是q
或者bluebird
这类JS库提供的Promise,情况则不一样了。)
参考文章: