译者:焉逢
原文链接
欢迎回到⎣事件循环⎤文章系列!在第一篇文章中,我整体介绍了 Node 的事件循环。在这篇文章中,我准备用一些代码片段作为例子,来详细探讨我们前文提及的三个重要的事件队列 —— 定时器队列、immediates 队列以及 nextTick 队列。
本系列文章指引
-
事件循环总体概览
-
定时器, Immediates 和 Process.nextTick (本文)
-
Resolved Promises 和 Process.nextTick — 未完成
-
I/O 的处理 — 未完成
-
处理事件循环的最佳实践 — 未完成
-
编写异步组件(async Add-ons) — 未完成
nextTick 队列
先让我们再次看看前文出现过的图示。
图中,nextTick 队列被区分开来,因为它是由 Node 实现而非 libuv 原生提供。
对于事件循环的每个阶段(定时器队列,I/O 事件队列,immediates 队列,close 处理队列是 4 个主要阶段),事件循环在移动到某一阶段之前,Node 会检查 nextTick 队列中是否有可处理的事件。如果有,在前往下一个主要阶段之前,事件循环会先直接处理这个队列直至它为空(当然还有其他 microtask 队列)。
这带来了一个新的问题。递归、无限的调用 process.nextTick
往 nextTick
队列添加事件将导致 I/O 和其他队列永远处于饥饿(starve)状态。我们可以用下面的代码模拟这种情况。
const fs = require('fs');
function addNextTickRecurs(count) {
let self = this;
if (self.id === undefined) {
self.id = 0;
}
if (self.id === count) return;
process.nextTick(() => {
console.log(`process.nextTick call ${++self.id}`);
addNextTickRecurs.call(self, count);
});
}
addNextTickRecurs(Infinity);
setTimeout(console.log.bind(console, 'omg! setTimeout was called'), 10);
setImmediate(console.log.bind(console, 'omg! setImmediate also was called'));
fs.readFile(__filename, () => {
console.log('omg! file read complete callback was called!');
});
console.log('started');
你可以看到输出是一个无限循环的 nextTick
回调调用,而 setTimeout
、setImmediate
和 fs.readFile
回调从未被调用,因为任何 ""omg!..."" 都没有打印到控制台中。
started
process.nextTick call 1
process.nextTick call 2
process.nextTick call 3
process.nextTick call 4
process.nextTick call 5
process.nextTick call 6
process.nextTick call 7
process.nextTick call 8
process.nextTick call 9
process.nextTick call 10
process.nextTick call 11
process.nextTick call 12
....
你可以尝试设置一个有限的值作为 addNextTickRecurs
调用的参数,你将看到 setTimeout
、setImmediate
和 fs.readFile
回调会在一系列 process.nextTick 回调后面被调用。
定时器队列
每当你使用 setTimeout
或 setInterval
添加定时器回调时,Node 将添加定时器到定时器堆(timer heap)中,这是一个由 libuv 访问的数据结构。在事件循环的定时器阶段,Node 会检查定时器堆中已过期的 timer/interval ,并调用它们各自的回调。如果有多个已过期的定时器(比如设置了相同的过期时间),那它们会按照它们被设置的顺序执行。
当一个 timer/interval 设置了明确的到期时间时,并不能确保在到期后能准时地执行回调。定时器回调的调用,依赖于系统的性能(Node 在执行回调前需要检查时间,显然这需要一些 CPU 时间)以及当前事件循环中的执行情况。事实上,到期时间确保的是,定时器回调至少在该时间之前,不会被调用。我们可以模拟一下:
const start = process.hrtime();
setTimeout(() => {
const end = process.hrtime(start);
console.log(`timeout callback executed after ${end[0]}s and ${end[1]/Math.pow(10,9)}ms`);
}, 1000);
上面的代码设置了一个过期时间为 1000ms 的定时器,它会打印出执行回调时消耗的时间。如果你运行该代码多次,你会注意到它每次的结果都不尽相同,并且永远不会出现 timeout callback executed after 1s and 0ms
这样的结果。你看到的结果类似这样:
timeout callback executed after 1s and 0.006058353ms
timeout callback executed after 1s and 0.004489878ms
timeout callback executed after 1s and 0.004307132ms
...
当 setTimeout
和 setImmediate
一起使用时,定时器的这种特性可能会导致意外和不可预测的结果,接下来你就会看到了。
Immediates 队列
尽管在行为上,immediates 队列和定时器队列有点相似,但它也有自己的一些独特特性。不像定时器,即使过期时间为0,我们也不能确保它的回调何时会执行,但 immediates 队列被确保会在事件循环的 I/O 阶段之后被立即执行。可以通过 setImmediate
向此队列添加一个事件(回调):
setImmediate(() => {
console.log('Hi, this is an immediate');
});
setTimeout vs setImmediate ?
现在,我们回过头来看看文章开头的那个图示,可以看到当事件循环开始执行的时候,Node 首先会处理定时器队列。接着,在处理完 I/O 之后,会来到 immediates 队列。通过图示,我们很容易推断出以下代码的输出结果。
setTimeout(function() {
console.log('setTimeout')
}, 0);
setImmediate(function() {
console.log('setImmediate')
});
你可能会猜,以上代码永远会在 setImmediate
之前打印 setTimeout
,因为定时器队列的处理优先于 immediates 队列。然而事实是,以上代码的运行结果是永远不确定的!如果你试着运行多次,你会得到不一样的输出结果。
这是因为设定一个 0 秒过期的定时器,永远不能保证在 0 秒后回调会被准时调用。正因如此,当事件循环启动时,它可能还没有看到已过期的定时器。这时候事件循环将前往 I/O 阶段,接着来到 immediates 队列,发现这队列中有一个待处理的事件,于是执行它。
但对于下面这段代码,我们可以确保 immediate 回调永远会比定时器回调先执行。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0);
setImmediate(() => {
console.log('immediate')
})
});
我们来看看它的执行流程。
-
一开始,程序通过
fs.readFile
异步读取当前文件,并提供了一个回调 -
事件循环启动
-
一旦文件读取完成,一个事件(待执行的回调)会被添加进事件循环的 I/O 队列中
-
由于没有其他待执行的事件,Node 开始等待后续的 I/O 事件。当它看到文件读取完成的事件时,执行它
-
在这个回调的执行过程中,一个定时器被添加到定时器堆中,一个 immediate 事件被添加到 immediates 队列中
-
我们知道当前事件循环处于 I/O 阶段,并且此时没有其他 I/O 事件可处理,事件循环于是来到 immediates 阶段,它发现了在执行文件读取回调的过程中被添加的 immediate 回调,于是便直接执行它
-
在事件循环的下一轮中,它发现了已过期的定时器,于是执行相应的回调
总结
让我们一起看看这些不同的阶段、队列,在整个事件循环中是如何工作的。以下是代码示例。
setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => console.log('this is set immediate 2'));
setImmediate(() => console.log('this is set immediate 3'));
setTimeout(() => console.log('this is set timeout 1'), 0);
setTimeout(() => {
console.log('this is set timeout 2');
process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
}, 0);
setTimeout(() => console.log('this is set timeout 3'), 0);
setTimeout(() => console.log('this is set timeout 4'), 0);
setTimeout(() => console.log('this is set timeout 5'), 0);
process.nextTick(() => console.log('this is process.nextTick 1'));
process.nextTick(() => {
process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
});
process.nextTick(() => console.log('this is process.nextTick 2'));
process.nextTick(() => console.log('this is process.nextTick 3'));
process.nextTick(() => console.log('this is process.nextTick 4'));
以上代码执行完之后,以下这些事件会被添加到相应的事件队列中。
- 3 个 immediate 事件
- 5 个定时器事件
- 5 个 nextTick 事件
我们看看执行流程:
-
事件循环启动时,它注意到 nextTick 队列非空,于是开始处理这个队列。在执行第二个 nextTick 回调的过程中,一个新的 nextTick 被添加到该队列的末尾,它将在当前 nextTick 队列结束后执行
-
开始执行定时器回调。在执行第二个定时器回调的过程中,一个事件被添加至 nextTick 队列
-
一旦定时器队列处理完,事件循环看到 nextTick 队列中有一个事件(在执行第二个定时器回调的过程中被添加的),于是处理 nextTick 队列
-
由于没有任何 I/O 事件可处理,事件循环来到了 immediates 阶段,并处理相应队列
很好,如果你运行以上代码,你将看到下面的输出结果:
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick 4
this is the inner next tick inside next tick
this is set timeout 1
this is set timeout 2
this is set timeout 3
this is set timeout 4
this is set timeout 5
this is process.nextTick added inside setTimeout
this is set immediate 1
this is set immediate 2
this is set immediate 3
下篇文章我们将讨论更多关于 nextTick 回调和 promise 的内容。如果你发现了有什么需要更正或添加的内容,请随时回复。
参考链接:
-
NodeJS API Docs nodejs.org/api
-
NodeJS Github github.com/nodejs/node…
-
Libuv Official Documentation docs.libuv.org/
-
NodeJS Design Patterns www.packtpub.com/mapt/book/w…
-
Everything You Need to Know About Node.js Event Loop — Bert Belder, IBM www.youtube.com/watch?v=PNa…
-
Node’s Event Loop From the Inside Out by Sam Roberts, IBM www.youtube.com/watch?v=P9c…
-
asynchronous disk I/O blog.libtorrent.org/2012/10/asy…
-
Event loop in JavaScript acemood.github.io/2016/02/01/…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!