欢迎回到 Event Loop 系列文章!在第一篇文章中,我描述了关于 NodeJS 的一个整体情况。在这篇文章中,我将详细讨论一下在第一篇文章中提到的三个重要的队列的细节,其中会包含一些代码片段。它们分别是 timers, immediates 和 process.nextTick 回调。
Next Tick 队列
让我们来看一下我们之前看过的 event loop 示例图。
Next tick 队列和另外四个主要队列分开放置,因为它不是 libuv 的原生提供的一部分,而是在 Node 中实现的。
在每个事件循环的阶段(timers 队列,IO events 队列, immediates 队列,close handlers 队列是主要的四个队列),在移动到这四个阶段之前,Node 会去检查 nextTick 队列上是否有任何队列事件。如果 nextTick 队列不是空的,Node 将会开始执行事件直到该队列清空,然后再会移动到主事件循环阶段当中。
这里说一下一个新问题。反复(Recursively/Repeatedly)通过 process.nextTick 函数添加事件到 nextTick 队列中会导致 I/O 和其他队列会永远不会执行。【注:startve forever 直译感觉有点奇怪,永远饥饿?还是干脆异译吧】。我们可以假设下面简单的 script 脚本是这种情况。
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 ...
你可以尝试给 addNextTickRecurs 设置一个有限的值作为参数,然后你可以看到 setTimeout, setTmmediate 和 fs.readFile 回调会在 proceess.nextTick call * 的 log 信息结束后被调用。
tips: 在 Node v0.12 之前,有一个属性叫做 process.maxTickDepth 可以设置 process.nextTick 队列长度的最大值。这个需要开发者手动设置 ,以便 Node 在给定点处理来自 next tick 队列的 maxTickDepth 回调。 但是在 Node v0.12 版本因为一些原因被移除了,所以新版的 Node 上,只有避免反复添加 event 到 next tick 队列(才能阻止这个问题)(repeatedly adding events to next tick queue is only discouraged)【注:这部分上篇文章提到过的】
Timers 队列
当你通过 setTimeout 添加一个 timer 或者通过 setTinterval 添加 interval,Node 将会在 timer 堆上新增 timer,这是通过 libuv 访问的数据结构【注:这里的意思个人理解应该是这个 timer 堆是通过 libuv 去访问读取的】。在 event loop 的 timers 阶段,Node 将会检查 timer 堆上是否有到期的 timers/intervals 然后分别调用他们的回调。如果这里出现了多个 timer 过期了(比如设置了相同的有效期),它们将会按照设置的顺序去执行。
当一个 timer/interval 设置了一个特定的有效期,其实并不会保证回调在过了有效期之后会完全执行。timer 的回调什么时候被调用取决于系统的性能(Node 在执行 callback 之前需要去检查 timer 的有效期,将会花费一些 CPU 时间)以及事件循环中当前正在进行的进程。然而,有效期将会确保 timer 的回调至少不会在设置的有效期期间被触发。我们可以用下面的程序假设这种情况。
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);
上面的程序将会执行一个 1000 ms 的计时器以及打印执行回调需要的时间。如果你多次运行这个程序,会发现每次打印的时间都不一样,并且它绝不会打印 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 队列在某些方面的行为和 timeouts 一样,它也有自己的独特的部分。不像 timer 我们无法预测当 timer 过期时间即使设置为 0 时的回调何时执行,immedediates 队列确保在 event loop 的 I/O 阶段执行后马上执行。可以通过下述 setImmediate 函数的用法给 immediates 队列添加事件:
setImmediate(() => { console.log('Hi, this is a immediate'); })
setTimeout 和 setImmediate 对比?
现在,当我们再去看顶部的事件循环的示意图时,你可以看到当程序开始执行的时候,Node 开始执行 timers。然后执行 I/O,然后才是 immediates 队列。看着示意图,我们可以简单的推测下面程序的输出。
setTimeout(function() { console.log('setTimeout') }, 0); setImmediate(function() { console.log('setImmediate') });
你也许会猜测,这个程序总是打印 setTimeout 在 setImmediate 之前,因为过期的计时器回调在 immediates 之前执行。但是程序的输出总是无法确定的。如果你执行这个程序多次,你会获取到不同的输出。
这是因为过期时间为 0 的计时器是无法确保在 0 秒后回调会立刻执行。因为这个,当 event loop 启动时或许不会马上看到过期的计时器(expired timer)。然后 event loop 将会移动到 I/O 阶段然后到 immediates 队列。然后它将会看到 immediates 队列中的事件并执行。
但是如果我们看下面的程序,我们确保 immediate 回调肯定在 timer 回调之前执行。
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0); setImmediate(() => { console.log('immediate') }) });
让我们看一下程序执行过程。
- 最开始,程序使用 fs.readFile 函数异步读取当前文件,并且提供了一个回调函数供文件读取后调用。
- 然后 event loop 启动。
- 一旦文件被读取,它将添加事件(即将被执行的回调)到 event loop 的 I/O 队列的中
- 当这里没有事件需要被执行了,Node 将会等待任何的 I/O 事件。它将会看到 I/O 队列中的文件读取事件并执行它。
- 在执行回调期间,一个 timer 被添加到 timers 堆中并且一个 immediate 被添加到 immediates 队列中
- 现在我们知道 event loop 到达了 I/O 阶段。当它没有任何 I/O 事件需要被执行,event lopp 将会移动到 immediates 阶段,它在执行文件读取回调期间看到添加的 immediate 回调事件。然后 immediate 回调将会执行。
- 在下一轮的 event loop 中,它将看到过期计时器然后将会执行 timer 回调。
结尾
所以让我们看一下这些不同的阶段/队列在 event loop 中是如何一起工作的。看下面这个例子:
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'));
当执行下面的脚本,下面的事件将被添加到各自的 event lopp 队列中。
- 3 个 immediates
- 5 个 timer 回调
- 5 个 next tick 回调
让我们看一下执行流程:
- 当 event loop 启动,它将注意到 next tick 队列并且执行 next tick 回调。在执行第二个 next tick 回调的过程中,一个新的 next tick 就会掉被添加到 next tick 队列末尾并且将在 next tick 队列的末尾执行。
- expired timers 回调将会被执行。在执行第二个 timer 回调期间,一个事件被添加到 next tick 队列中。
- 一旦所有的 expired timer 中的事件都被执行完,event loop 将会看到 next tick 队列中还有一个事件(就是第二次 timer 回调中添加的),然后 event loop 将会执行它。
- 当没有任何 I/O 事件被执行,event loop 将会移动到 immediates 阶段并执行 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
让我们在下篇文章中讨论更多关于 next tick 回调及 resolved promises 的内容。