什么是事件循环
众所周知,JavaScript 是单线程的,而 Nodejs 又可以实现无阻塞的 I/O 操作,就是因为 Event Loop 的存在。
Event Loop 主要有以下几个阶段,一个矩形代表着一个阶段,如下所示:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
每个阶段都会维护一个先进先出的队列结构,当事件循环进入某个阶段时,就会执行该阶段的一些特定操作,然后依序执行队列中的回调函数。 当队列中的回调函数都执行完毕或者已执行的回调函数的个数达到某个最大值,就会进入下一个阶段。
timers
事件循环的开始阶段,执行 setTimeout
和 setInterval
的回调函数。
当定时器规定的时间到了之后,就会将定时器的回调函数放入队列中,然后依序执行。
假如你有 a、b、c 四个定时器,时间间隔分别为 10ms 、20ms 、 30ms。当进入事件循环的 timer
阶段时,时间过去了 25 ms,那么定时器 a 和 b 的回调就会被执行,执行完毕后就进入下一个阶段。
I/O callbacks
执行除了 setTimeout
、 setInterval
、 setImmediate
和 close callbacks
等的回调函数。
idle, prepare
进行一些内部操作。
poll
这应该是事件循环中最重要的一个阶段了。
如果这个阶段的队列不为空,那么队列中的回调会被顺序执行;如果队列为空,也有 setImmediate
函数被调用,那么就会进入 check
阶段。如果队列为空且没有 setImmediate
的函数调用,事件循环会进行等待,一旦有回调函数被添加到队列中时,立即执行。
check
setImmediate
的回调会在这个阶段被执行。
close callbacks
例如 socket.on('close', ...)
等的回调在这个阶段执行。
setTimout vs setImmediate
// timeout_vs_immediate_1.js
setTimeout(function timeout() {
console.log('timeout');
}, 0);
setImmediate(function immediate() {
console.log('immediate');
});
按照之前的说法,事件循环会先进入 timer
阶段,执行 setTimeout
的回调,等到进入 check
阶段时, setImmediate
的回调才会被执行。所以有一些人认为上面的代码的输出结果应该是:
$ node timeout_vs_immediate_1.js
timeout
immediate
但其实这里的结果是不确定的。这里往往跟进程的性能有关系,而且,这里 setTimeout
的间隔虽然是 0,实际上会是 1。所以当启动程序进入事件循环,时间还未过去 1ms 时,timer 阶段的队列是空的,不会有回调被执行。而这里又有 setImmediate
函数的调用,所以之后走到 check
阶段时,setImmediate
的回调会被调用。如果事件循环进入 timer
阶段时,已经消耗了 1ms ,那么这个时候 setTimeout
的回调就会被执行,之前进入到 check
阶段,再执行 setImmediate
的回调。
所以,以下的两种输出都可能出现。
$ node timeout_vs_immediate_1.js
timeout
immediate
$ node timeout_vs_immediate_1.js
immediate
timeout
假如,上面的代码是放在一个 I/0 循环内,如
// timeout_vs_immediate_2.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
那么结果就是确定的,输出结果如下
$ node timeout_vs_immediate_2.js
immediate
timeout
process.nextTick()
process.nextTick()
并不属于事件循环中的一部分,但也是异步的 API 。
当前的操作完成了之后,如果有 process.nextTick()
的调用,那么 process.nextTick()
中的回调会接着被执行,如果回调里面又有 process.nextTick()
,那么回调中的 process.nextTick()
的回调也会接着被执行。所以 procee.nextTick()
可能会阻塞事件循环进入下一个阶段。
// process_nexttick_1.js
let i = 0;
function foo() {
i += 1;
if (i > 3) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
process.nextTick(foo);
}
setTimeout(foo, 5);
按照之前的说法,上面的输出结果如下:
$ node process_nexttick_1.js
foo func
foo func
foo func
timeout
timeout
timeout
你可能会有一个疑问,process.nextTick()
是在事件循环某个阶段的队列都清空之后再执行,还是在队列中某个回调执行完成后接着执行。想想下面的代码的输出结果是什么?
// process_nexttick_2.js
let i = 0;
function foo() {
i += 1;
if (i > 2) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
process.nextTick(foo);
}
setTimeout(foo, 2);
setTimeout(() => {
console.log('another timeout');
}, 2);
执行一下,看看结果
// node version: v11.12.0
$ node process_nexttick_2.js
foo func
foo func
another timeout
timeout
timeout
结果如上所示,process.nextTick()
是在队列中的某个回调完成后就接着执行的。
你看到上面的结果,有个注释 node version: v11.12.0
。 即运行这段代码的 node 版本为 11.12.0 ,如果你的 node 版本是低于这个的,如 7.10.1 ,可能就会得到不同的结果。
// node version: v7.10.0
$ node process_nexttick_2.js
foo func
another timeout
foo func
timeout
timeout
不同的版本表现不同,我想应该是新的版本做了更新调整。
process.nextTick() vs Promise
process.nextTick()
对应的是 nextTickQueue,Promise
对应的是 microTaskQueue 。
这两者都不属于事件循环的某个部分,但它们执行的时机都是在当前的某个操作之后,那这两者的执行先后呢
// process_nexttick_vs_promise.js
let i = 0;
function foo() {
i += 1;
if (i > 2) return;
console.log('foo func');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(foo);
}
setTimeout(foo, 0);
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nexttick');
});
运行代码,结果如下:
$ node process_nexttick_vs_promise.js
nexttick
promise
foo func
foo func
timeout
timeout
如果你都搞懂了上面的输出结果是为何,那么对于 Nodejs 中的事件循环你也就可以掌握了。
参考资料
- The Node.js Event Loop, Timers, and process.nextTick()
- Node.js event loop workflow & lifecycle in low level
讨论
欢迎大家一起讨论,有不错的地方请指正。