Event Loop 允许Node.js执行非阻塞I/O操作, 尽管JavaScript是单线程的,只要有可能就将操作卸载到系统内核。
由于大多数现代内核是多线程的,所以它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会通知Node.js,以便可以将相应的回调(callbacks)添加到轮询队列(poll queue)中以最终执行。事件循环每次循环称为一次Tick 我们稍后将在本主题中进一步详细解释。

Event Loop的解释

英文原文:
When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段

┌───────────────────────┐
┌─>│ timers │ (setTimeout(), setInterval())
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │ (setImmediate())
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

阶段概览

  • timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
  • I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

注意:

  • 上面六个阶段都不包括 process.nextTick()

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,
node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,
event loop会转入下一下阶段.

由于这些操作中的任何一个都可以调度更多操作,并且在轮询阶段中处理的新事件由内核排队,所以轮询事件可以在轮询事件被处理的同时排队。因此,长时间运行的回调可以使轮询阶段(poll)的运行时间远远超过计时器的阈值.有关更多详细信息,请参阅定时器和轮询部分。

在事件循环的每次运行之间,Node.js检查是否正在等待任何异步I/O或定时器,如果没有任何异步I/O或定时器,则清除关闭。

各阶段详情

times

计时器指定阈值(threshold),然后执行提供的回调(callback),但是可能不是我们希望它执行的确切时间。也就是说定时器的时间是有误差的.
定时器回调会在指定的时间过后按照预定的时间运行; 但是,操作系统调度或其他回调的运行可能会延迟它们。 注意:从技术上讲,poll阶段控制何时执行定时器。

例子: 假设您计划在100 ms阈值后执行超时,然后异步开始读取需要95 ms的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(function() {
const delay = Date.now() - timeoutScheduled;
console.log(delay + 'ms have passed since I was scheduled');
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(function() {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});

当事件循环进入轮询阶段时,它有一个空的队列(fs.readFile()还没有完成),所以它将等待剩余的毫秒数,直到达到最快的定时器的阈值.当它等待95ms传递时,fs.readFile()完成读取文件,并且其需要10ms完成的回调被添加到轮询队列并被执行。 当回调完成时,队列中没有更多的回调,所以事件循环会看到最后一个定时器的阈值已经达到,然后回到定时器阶段执行定时器的回调。在这个例子中,你会看到被调度的定时器和它正在执行的回调之间的总延迟将是105ms。

I/O callbacks

处理一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误callback 例如,如果尝试连接时TCP套接字收到ECONNREFUSED,则某些*nix系统要等待报告错误。这将排队在I/O回调阶段执行。

idle, prepare

node 内部调用 忽略

poll

poll阶段有两个主要功能:

  • 处理轮询队列(poll queue)中的事件(callback)
  • 执行阈值已过的定时器timers的事件(callback)

当事件循环进入poll阶段并且代码未设置计时器时,会发生以下两种情况之一:

  • 如果轮询队列(poll queue)不是空的,则事件循环将遍历队列并同步执行它们的回调队列,直到队列被清空或达到系统相关的硬限制。
  • 如果轮询队列(poll queue)为空,则会发生以下两件事情之一:
    • 如果脚本已由setImmediate()调度,则事件循环将结束poll阶段并执行check阶段(check阶段的queue是 setImmediate设定的)
    • 如果脚本没有被setImmediate()调度,事件循环将等待callback被添加到队列,然后立即执行。

当事件循环进入poll阶段并且轮询队列(poll queue)为空, 事件循环将检查已达到时间阈值的定时器. 如果一个或多个定时器准备就绪, 则事件循环将回到timers阶段以执行这些定时器的回调(callbacks).

check

check阶段允许代码在poll queue空闲后立即执行回调。也就是setImmediate() 如果poll queue空闲并且代码中已经设置了setImmediate(),则事件循环直接进入到check阶段。 setImmediate()实际上是一个特殊的定时器,它在事件循环的一个单独的阶段中运行。 它使用一个libuv API来调度poll阶段完成后执行的回调。 通常,随着代码的执行,事件循环将最终进入poll阶段,在那里它将等待传入连接,请求等。但是,如果使用setImmediate()设置了回调并且poll queue空闲,将直接进入check阶段,而不是等待poll事件。

setImmediate和setTimeout

setImmediate和setTimeout 是相似的,但取决于它们何时被调用,以不同的方式运行.

  • setImmediate() 用于在当前的poll阶段完成后执行脚本.
  • setTimeout() 计划一个脚本,以ms为单位的最小阈值运行.

setTimeout(fun(), 0) === setTimeout(fun(), 1)
另外, setTimeout(), HTML5中会有最低限制4ms, 这与浏览器有关, Node 中setTimeout通过libuv模块实现, 模块最终调用平台底层的高精度定时器, 并不会有这个限制. 详情(http://www.developerq.com/article/1488429596)

以下例子的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log(0)
}, 0);
setImmediate(function() {
console.log(1)
});
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);

结果:(忽略换行)
理论上235401 / 235410
实际由于1ms时间很短, 所以235401

分析:

> setTimeout(func(), 0) === setTimeout(func(), 1), 并不会直接运行, 会将func()放入times queue;

> setImmediate()是check阶段运行, 会将func()放入check queue;

> new Promise() Promise创建就会直接执行, 所以会输出 2; 执行for循环, i === 9999时, resolve()返回一个新的Promise对象, 但是.then() 是异步执行的, 也就是会把下一个.then()放到当前的poll queue中, 等待当前poll阶段执行完, 然后输出35, 此时poll阶段执行完, 遍历poll queue输出4

> poll 阶段结束, 检查times阶段的定时器是否达到或超过设定的阈值, 如果超过设置的阈值, 则执行times callback 即输出 0, 然后进入check阶段 输出1; 01的输出顺序取决于执行第3步时的时间, 该时间 大于setTimuout()的阈值则先输出0, 否则先输出1; 尝试修改setTimeout(xx, 11), 则本机先输出1;

process.nextTick()

从技术上来说,它并不是event loop的一部分。相反的,process.nextTick()会把回调塞入nextTickQueue,nextTickQueue将在当前操作完成后处理,不管目前处于event loop的哪个阶段。

看看我们最初给的示意图,process.nextTick()不管在任何时候调用,都会在所处的这个阶段最后,在event loop进入下个阶段前,处理完所有nextTickQueue里的回调。

process.nextTick() vs setImmediate()

  • process.nextTick()立即在本阶段执行回调;
  • setImmediate()只能在 check 阶段执行回调。