Timers, Immediates,以及 Next Ticks

欢迎回到 Event Loop 系列文章!在第一篇文章中,我描述了关于 NodeJS 的一个整体情况。在这篇文章中,我将详细讨论一下在第一篇文章中提到的三个重要的队列的细节,其中会包含一些代码片段。它们分别是 timers, immediates 和 process.nextTick 回调。

Next Tick 队列

让我们来看一下我们之前看过的 event loop 示例图。

event loop2.png

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 回调

让我们看一下执行流程:

  1. 当 event loop 启动,它将注意到 next tick 队列并且执行 next tick 回调。在执行第二个 next tick 回调的过程中,一个新的 next tick 就会掉被添加到 next tick 队列末尾并且将在 next tick 队列的末尾执行。
  2. expired timers 回调将会被执行。在执行第二个 timer 回调期间,一个事件被添加到 next tick 队列中。
  3. 一旦所有的 expired timer 中的事件都被执行完,event loop 将会看到 next tick 队列中还有一个事件(就是第二次 timer 回调中添加的),然后 event loop 将会执行它。
  4. 当没有任何 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 的内容。

原文内容:https://jsblog.insiderattack.net/timers-immediates-and-process-nexttick-nodejs-event-loop-part-2-2c53fd511bb3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容