《Node.js事件循环》笔记

在Node.js中,事件循环是实现异步I/O的关键,是必须要了解的知识。
这篇笔记是我读官方文档里关于事件循环的文章得来的。

事件循环的概念

事件循环使单线程的JavaScript实现了异步I/O操作,通过将负载交给系统内核执行。由于大多数的现代操作系统都是多线程的,能在后台执行多任务的操作。当后台操作完成后,内核(kernel)通知Node.js,这样的话回调函数就可以添加到poll队列中,直到执行完成。

事件循环的执行顺序

事件循环

图片中的每个阶段称为phase (图片来自libuv文档)

每个阶段(phase)都有一个待执行的回调函数FIFO队列,每一个阶段都有所不同。事件循环每进行到一个阶段,就会执行当前阶段特有的操作,然后执行回调函数,直至将回调函数的队列清空或者达到设置的最大限制

timers

该阶段执行的是由*setTimeout()setInterval()*设置的回调函数。

从技术上来讲,poll阶段控制着timers阶段什么时候执行。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');

console.time('timeout');
setTimeout(() => {
console.timeEnd('timeout');
}, 900);

function readFile() {
fs.readFile('./timeout.js', (err, data) => {
console.log(JSON.parse(JSON.stringify({a: 1})));
});
}

readFile();

第一个setTimeout设置的时间是900ms,但是用console.timeEnd()的输出用时却是大于900ms的,在我的电脑上大约是902ms左右。这是因为当事件循环进行到poll阶段时,发现队列中没有任务,然后等待了一会儿(小于 900ms),然后读文件操作执行完了,并且将回调函数添加到了poll队列中,然后执行完该回到函数后(大概用了2ms)发现setTimeout()设置的时间到了,然后执行了其回调函数。所以setTimeout()的回调函数的执行等待了大于900ms的时间。

为了防止poll阶段饿死事件循环,libuv设置了poll阶段可以行的回调函数的最大数量限制,该限制在不同的的系统上不相同。

pending callbacks

执行延迟到下一个阶段的I/O回调。

该阶段执行一些系统操作的回调,比如TCP的链接错误。

idle,prepare

内部使用(不太明白内部使用是什么意思)。

poll

  • 获取新的I/O事件;
  • 执行几乎所有I/O相关的回调函数,除了:close callback,由timers和*setImmediate()*设置的回调。

Node.js可能会阻塞在该阶段。

当事件循环进行到该阶段,并且没有设置timers时,有可能会发生下面的两件事情之一:

  1. 如果poll队列不为空,事件循环会处理队列里的回调函数,直到处理完成或者达到系统的最大限制;
  2. 如果poll队列为空,又会发生下面的两件事情之一:
    1. 如果设置了setImmediate(),则事件循环会结束poll阶段,然后进入到check阶段,去执行setImmediate设置的回调函数;
    2. 如果没有设置*setImmediate()*,则事件循环会等待回调函数添加到队列,然后立即执行它们。

一旦poll队列为空,事件循环会去检查是否有timers到时间,如果有,就返回timers阶段执行到期的timers

check

执行*setImmediate()*设置的回调。

该阶段主要是用来执行poll阶段后需要立即执行的操作的。如果poll阶段闲置了,并且setImmediate()设置了一些需要执行的脚本,循环就会直接进入check阶段。setImmediate()其实是一个特殊的timer.

close callback

关闭事件的回调函数,比如socket.on('close', ...)