欢迎回到 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 队列以后被执行。
看一下下面的例子:
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