Promises, Next-Ticks 和 Immediates -- NodeJS Event Loop 第三部分

欢迎回到 Event Loop 文章系列!在第一篇文章中,我们对 Node JS event loop 进行了一个整体概括以及它的不同的阶段。接着在第二篇文章中,我们讨论了事件循环的上下文中的定时器(timers)和即时消息(immediates),以及每个队列的调度方式。在这篇文章中,我们将会看到事件循环如何调度 promises resolved/rejected (包括原生的 JS promises, Q promises 和 Bluebird promises)和 next tick 回调。如果你还不熟悉 Promise, 我建议你先去学习一下 Promises, 相信我,它真的非常 cool。

原生 Promises

在原生的 promises 上下文中,一个 promises 回调会被认为是一个微任务以及被放在微任务队列中去排队,并且将会在 next tick 队列以后被执行。

event loop2.png

看一下下面的例子:

Promise.resolve().then(() => console.log('promise1 resolved')); Promise.resolve().then(() => console.log('promise2 resolved')); Promise.resolve().then(() => { console.log('promise3 resolved'); process.nextTick(() => console.log('next tick inside promise resolve handler')); }); Promise.resolve().then(() => console.log('promise4 resolved')); Promise.resolve().then(() => console.log('promise5 resolved')); setImmediate(() => console.log('set immediate1')); setImmediate(() => console.log('set immediate2')); process.nextTick(() => console.log('next tick1')); process.nextTick(() => console.log('next tick2')); process.nextTick(() => console.log('next tick3')); setTimeout(() => console.log('set timeout'), 0); setImmediate(() => console.log('set immediate3')); setImmediate(() => console.log('set immediate4'));

在上面例子中,下面的事情将会发生:

1. 五个 handlers 将会被添加到 resolved promisese 微任务队列。(注意我添加的五个 resolve handlers 是到五个都是 resolved 的 promises 中)

2.两个 handlers 将会被添加到 setImmediate 队列

3.三个项目将会被添加到 process.nextTick 队列中

4.一个有效期为0秒的计时器被创建, 将会立即过期并将回调添加到 timers 队列

5.两个项目将会被添加到 setImmediate 队列。

然后事件循环将会开始检查 process.nextTick 队列。

1.循环将会发现 process.nextTick 队列中有三个项目,然后 Node 将会执行 process.nextTick 队列直到队列为空。

2.然后循环将会检查 promises 微任务队列并且发现 promises 微任务队列中有五个事件并开始执行

3.在执行 promises 微任务队列期间,一个项目又被添加到 process.nextTick 队列中(next tick 是在 promise resolve 处理函数中)

4.在 promises 微任务队列结束后,事件循环将会再次发现刚才执行 promises 微任务时期添加到 process.nextTick 的那个项目,然后 node 将会执行这个 nextTick 队列中的事件。

5.在 promises 和 nextTicks 里的项全部执行完以后,事件循环将会移动到第一个阶段,也就是 timers 阶段。此时能看到这边有一个有回调函数在 timers 队列中,然后就去执行它。

6.现在没有任何剩余的 timer 回调了,循环将会等待 I/O。当我们没有任何需要等待执行的 I/O 时,循环将会移动到 setImmediate 队列。它将看到这边有四个项目在 immediate 队列中并且会执行它们直到队列清空。

7.最后,循环做完了所有时,程序退出。

Tips: 为什么我们总是看到这两个词 ”promises microtask“ 而不是单独的 ”microtask“ 呢?

我知道到处都看到它是难受的的,但是你需要知道 promises 和 resolved/rejected 和 process.nextTick 都是微任务,因此,我不能单独去说 nextTick 队列和微任务队列。

所以来让我看一下上面例子的输出吧:

next tick1 next tick2 next tick3 promise1 resolved promise2 resolved promise3 resolved promise4 resolved promise5 resolved next tick inside promise resolve handler set timeout set immediate1 set immediate2 set immediate3 set immediate4

Q 和 Bluebird

我们现在知道原生的 promises 的 resolve/reject 回调会被作为微任务去调取并且在循环进入到一个新阶段之前执行。那 Q 和 Bluebird 呢?

在 JS 为 NodeJS 提供原生的 promises 之前,以前人们都是使用 promises 库比如 Q 和 Bluebird。自从这些库被原生的 promise 替代了,它们和原生的 promise 比有不同的语义了。

在写本文时,Q(v1.5.0) 使用 process.nextTick 队列去调度 promises 的 resolved/rejected 回调。基于 Q 的文档:

tips: 注意 promise 永远是异步的:这是因为 fulfillment 或者 rejection handler 将会在下一次事件循环执行中被执行(例如:Node 中的 process.nextTick)。它会在你手动追踪代码执行过程中一个好的保证,命名一个 then 将会处理之前执行完的结果。【注:用得太多了,不多做解释了】

另一方面,Bluebird 在写文本时(v3.5.0) 在最近的 Node 版本中默认使用 setImmediate 去调度 promises 的回调,(你可以在这里看到代码 here)。

为了对这张图分析得更清楚一些,我们来看一下另一个例子。

const Q = require('q'); const BlueBird = require('bluebird'); Promise.resolve().then(() => console.log('native promise resolved')); BlueBird.resolve().then(() => console.log('bluebird promise resolved')); setImmediate(() => console.log('set immediate')); Q.resolve().then(() => console.log('q promise resolved')); process.nextTick(() => console.log('next tick')); setTimeout(() => console.log('set timeout'), 0);

在上面的例子中,BlueBird.resolve().then 回调和接下去的 setImmediate 调用有相同的语义。因此,Bluebird 的回调在 setImmediate 回调之前被调度进相同的 immediates 队列中。自从 Q 使用 promise.nextTick 去调度 resolce/reject 回调,Q.resolve().then 在 process.nextTick 回调成功之前被调度进 nextTick 队列中。我们可以减少代码来看一下上面的程序的真实输出,如下:

q promise resolved next tick native promise resolved set timeout bluebird promise resolved set immediate

tips: 注意及时我在上面的例子中只使用 promise resolve 处理函数,这个行为同样适用于 promise reject 处理函数。最文章的最后,我将给出一个同时包含 resolve 和 reject 的输出。

Bluebird 给了我们一个选择,我们可以选择自己调度编排。做这些意味着我们可以使用 process.nextTick 而不是 setImmediate 吗?是的。Bluebird 提供一个 API 方法叫做 setScheduler,它可以获取一个函数去修改默认的 setImmediate 调度。

使用 process.nextTick 作为 bluebird 的调度者,你可以这样修改:

const BlueBird = require('bluebird'); BlueBird.setScheduler(process.nextTick);

使用 setTimeout 作为 bluebird 的调度者你可以这样写:

const BlueBird = require('bluebird'); BlueBird.setScheduler((fn) => { setTimeout(fn, 0); });

tips: 为了避免一篇文章太长,我不会在这边给出一个不同的 blurbird 调度的例子。你可以自己尝试去使用一下。

使用 setImmediate 而不是 process.nextTick 在 node 最新的版本上使用是有一些优点的。自从 NodeJS v0.12 及以上不提供 process.maxTickDepth 参数,在事件循环中添加太多事件到 nextTick 队列中会导致 I/O 饥饿。因此,如果在 node 最新版本上使用 setImmedita 而不是 process.nextTick 是安全的,因为如果没有任何 nextTick 回调,immediates 队列会在 I/O 事件之后获取执行权,setImmediate 将永远不会导致 I/O 饥饿。

最后一个转折

如果你运行下面的程序,你将会发现一个令人费解的输出:

const Q = require('q'); const BlueBird = require('bluebird'); Promise.resolve().then(() => console.log('native promise resolved')); BlueBird.resolve().then(() => console.log('bluebird promise resolved')); setImmediate(() => console.log('set immediate')); Q.resolve().then(() => console.log('q promise resolved')); process.nextTick(() => console.log('next tick')); setTimeout(() => console.log('set timeout'), 0); Q.reject().catch(() => console.log('q promise rejected')); BlueBird.reject().catch(() => console.log('bluebird promise rejected')); Promise.reject().catch(() => console.log('native promise rejected'));

输出是这样子的:

q promise resolved q promise rejected next tick native promise resolved native promise rejected set timeout bluebird promise resolved bluebird promise rejected set immediate

现在你应该有两个疑问?

1.如果 Q 在 promise 的 resolved/rejected 回调函数里面使用 process.nextTick,log 将会如何输出呢?”q promise rejectd“ 在 ”next tick“ 之前。

2. 如果 Bluebird 在 Promise resolved/rejected 回调函数中使用 setImmediate,这边又会如何输出呢?”bluebird promise rejected“ 会在 ”set immediate“ 之前。

这是因为两个库在内部对数据结构中的 promise resolved/rejected 进行排队,并且使用 process.nextTick 或者 setImmediate 去处理队列。

现在你知道了很多关于 setTimeout, setImmediate, process.nextTick 以及 promises,你应该对上面给的例子有很清晰的理解。下一篇文章中,我将详细讨论如何用事件循环处理 I/O。

原文地址:https://jsblog.insiderattack.net/promises-next-ticks-and-immediates-nodejs-event-loop-part-3-9226cbe7a6aa

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

推荐阅读更多精彩内容

  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 7,313评论 6 19
  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,708评论 1 56
  • 本文作者就是我,简书的microkof。如果您觉得本文对您的工作有意义,产生了不可估量的价值,那么请您不吝打赏我,...
    microkof阅读 15,949评论 9 40
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,713评论 0 5
  • 欢迎回到 Event Loop 系列文章!在第一篇文章中,我描述了关于 NodeJS 的一个整体情况。在这篇文章中...
    吃柠檬的刺猬阅读 579评论 0 1