Nodejs 事件循环(第三部分)- Promise,nextTicks,immediate
原文链接
文章系列路线图
- NodeJS 事件循环(第一部分)- 事件循环机制概述
- NodeJS 事件循环(第二部分)- Timers,Immediates,nextTick
- Nodejs 事件循环(第三部分)- Promise,nextTicks,immediate
- NodeJS 事件循环(第四部分)- 处理 IO
- NodeJS 事件循环(第五部分)- 最佳实践
欢迎回到事件循环系列文章!在这个系列的第 1 部分,我们讨论了 Node.js 事件循环的整体情况。在之后的第 2 部分,我们在事件循环的上下文中讨论了计时器(timers)和immediates,以及每个队列是如何调度的。接下来在这篇文章中,让我们看下事件循环如何调度 完成的/失败的 promise(包括原生 JS promise,Q promises 和 Bluebird Promise)和下一轮回调(next Tick callback)。
原生 Promises
在原生 promises 的上下文中,一个 promise 的回调被看成一个微任务和在微任务队列中入队,在 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'));
在上面的示例中,会发生下面的行为。
- 5 个回调处理器被添加到完成的 promise 的微队列中。(注意我添加了 5 个完成的回调处理器到 5 个已完成的 promise 中)
- setImmediate 队列中被添加 2 个回调处理器。
- process.nextTick 队列中被添加了 3 个回调处理器。
- 1 个带有过期时间为 0 的计时器,回调被加入到了计时器队列。
- setImmediate 队列中被添加了 2 个回调处理器。
然后事件循环将会开始检查 process.nextTick 队列。
- 循环将会辨识出在 process.nextTick 队列中有 3 项,Node 将会开始处理 nextTick 队列直到队列为空。
- 然后事件循环将会检查 promises 微队列并且辨识出有 5 项,然后开始处理队列。
- 在 promises 微队列处理过程中,process.nextTick 队列中再次被添加一项。
- promises 微队列被处理完后,事件循环会再次检测到 process.nextTick 队列中有一项(在处理 promises 微队列过程中添加的)。然后 node 将会处理 nextTick 队列中剩余的一项。
- 足够多的 promises 和 nextTicks。这里没有剩余的微任务了。然后事件循环移到第一个阶段,timers 阶段。这时,它会发现在计时器队列中有一个过期的 timer 回调,然后会处理这个回调。
- 现在没有其他定时器回调了,循环将会等待 I/O。因为我们没有任何的即将发生的(pengding) I/O,循环会继续,开始处理 setImmediate 队列。它在 immediate 队列中发现了 4 项,并且处理它们,直到 immediate 队列为空。
- 最后,循环做完了一切事情... 然后程序优雅地退出。
看到这两个词“promises 微任务”就足够了,而不是“微任务”。
我知道到处看到它是个痛苦,但是你知道 resolved/rejected promise 和 process.nextTick 都是微任务。因此相信我,我不能只说 nextTick 队列和 微任务(microtask)队列。
因此,我们看下上面示例的输出。
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
酷!我们知道 JS 原生的 promise 的 resolve/ reject 回调将会被作为微任务调度,并且在 loop 移到新的阶段之前处理它们。那么 Q 和 Bluebird ?
在 NodeJS 中实现原生的 JS promises 之前, 之前的开发者用一些类似 Q 和 Bluebird 的库。因为这些库在时间上早于原生的 promise,它们与原生的 promises 有不同的语义。
Q(v15.0)使用 promise.nextTick 队列调度 promises 的 resolve/reject 回调。
注意,promises 的解决永远是异步的。
换句话说,Bluebird(v3.5.0)使用 setImmediate 默认调度 promise 回调。
为了搞清楚,我们看另一个示例。
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 的回调在 immediate 队列中调度。因为 Q.resolve().then 在 nextTick 队列中调度。我们可以得出推论,看下面的输出:
q promise resolved
next tick
native promise resolved
set timeout
bluebird promise resolved
set immediate
注意,尽管我这里都用了 promise 的 resolve 回调,实际上 reject 回调和 resolve 回调处理方式一样,下面我会给你一个带有 resolve 和 reject 的例子。
Bluebird,然而,给我们提供了一个选择。我们可以选择自己的调度机制。是不是意味着我们可以指导 bluebird 使用 process.nextTick 代替 setImmediate?是的。Bluebird 提供一个命名为 setScheduler 的 API 方法,接受一个函数,覆盖默认的 setImmediate 调度器。
为了在 bluebird 使用 process.nextTick 作为调度器,你可以指定,
const BlueBird = require('bluebird');
BlueBird.setScheduler(process.nextTick);
为了在 bluebird 中使用 setTimeout 作为调度器,你可以使用下面的代码,
const BlueBird = require('bluebird');
BlueBird.setScheduler((fn) => {
setTimeout(fn, 0);
你可以自己写一些示例,然后观察不同的输出。
使用 setImmediate 代替 process.nextTick 在最新的 node 版本中有优势。因为 NodeJS v0.12 及以上的版本没有 process.maxTickDepth 的实现了,过分往 nextTick 队列中添加事件可能导致事件循环中的 I/O 饿死。因此在最新的版本中使用 setImmediate 比 process.nextTick 要安全。因为 setImmediate 队列在 I/O 后面处理的,所以不会饿死 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