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

歡迎回到 Event Loop 文章系列!在第一篇文章中,我們對 Node JS event loop 進行了一個整體概括以及它的不同的階段。接著在第二篇文章中,我們討論了事件循環(huán)的上下文中的定時器(timers)和即時消息(immediates),以及每個隊列的調(diào)度方式。在這篇文章中,我們將會看到事件循環(huán)如何調(diào)度 promises resolved/rejected (包括原生的 JS promises, Q promises 和 Bluebird promises)和 next tick 回調(diào)。如果你還不熟悉 Promise, 我建議你先去學習一下 Promises, 相信我,它真的非常 cool。

原生 Promises

在原生的 promises 上下文中,一個 promises 回調(diào)會被認為是一個微任務以及被放在微任務隊列中去排隊,并且將會在 next tick 隊列以后被執(zhí)行。

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'));

在上面例子中,下面的事情將會發(fā)生:

1. 五個 handlers 將會被添加到 resolved promisese 微任務隊列。(注意我添加的五個 resolve handlers 是到五個都是 resolved 的 promises 中)

2.兩個 handlers 將會被添加到 setImmediate 隊列

3.三個項目將會被添加到 process.nextTick 隊列中

4.一個有效期為0秒的計時器被創(chuàng)建, 將會立即過期并將回調(diào)添加到 timers 隊列

5.兩個項目將會被添加到 setImmediate 隊列。

然后事件循環(huán)將會開始檢查 process.nextTick 隊列。

1.循環(huán)將會發(fā)現(xiàn) process.nextTick 隊列中有三個項目,然后 Node 將會執(zhí)行 process.nextTick 隊列直到隊列為空。

2.然后循環(huán)將會檢查 promises 微任務隊列并且發(fā)現(xiàn) promises 微任務隊列中有五個事件并開始執(zhí)行

3.在執(zhí)行 promises 微任務隊列期間,一個項目又被添加到 process.nextTick 隊列中(next tick 是在 promise resolve 處理函數(shù)中)

4.在 promises 微任務隊列結束后,事件循環(huán)將會再次發(fā)現(xiàn)剛才執(zhí)行 promises 微任務時期添加到 process.nextTick 的那個項目,然后 node 將會執(zhí)行這個 nextTick 隊列中的事件。

5.在 promises 和 nextTicks 里的項全部執(zhí)行完以后,事件循環(huán)將會移動到第一個階段,也就是 timers 階段。此時能看到這邊有一個有回調(diào)函數(shù)在 timers 隊列中,然后就去執(zhí)行它。

6.現(xiàn)在沒有任何剩余的 timer 回調(diào)了,循環(huán)將會等待 I/O。當我們沒有任何需要等待執(zhí)行的 I/O 時,循環(huán)將會移動到 setImmediate 隊列。它將看到這邊有四個項目在 immediate 隊列中并且會執(zhí)行它們直到隊列清空。

7.最后,循環(huán)做完了所有時,程序退出。

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

我們現(xiàn)在知道原生的 promises 的 resolve/reject 回調(diào)會被作為微任務去調(diào)取并且在循環(huán)進入到一個新階段之前執(zhí)行。那 Q 和 Bluebird 呢?

在 JS 為 NodeJS 提供原生的 promises 之前,以前人們都是使用 promises 庫比如 Q 和 Bluebird。自從這些庫被原生的 promise 替代了,它們和原生的 promise 比有不同的語義了。

在寫本文時,Q(v1.5.0) 使用 process.nextTick 隊列去調(diào)度 promises 的 resolved/rejected 回調(diào)?;?Q 的文檔:

tips: 注意 promise 永遠是異步的:這是因為 fulfillment 或者 rejection handler 將會在下一次事件循環(huán)執(zhí)行中被執(zhí)行(例如:Node 中的 process.nextTick)。它會在你手動追蹤代碼執(zhí)行過程中一個好的保證,命名一個 then 將會處理之前執(zhí)行完的結果。【注:用得太多了,不多做解釋了】

另一方面,Bluebird 在寫文本時(v3.5.0) 在最近的 Node 版本中默認使用 setImmediate 去調(diào)度 promises 的回調(diào),(你可以在這里看到代碼 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 回調(diào)和接下去的 setImmediate 調(diào)用有相同的語義。因此,Bluebird 的回調(diào)在 setImmediate 回調(diào)之前被調(diào)度進相同的 immediates 隊列中。自從 Q 使用 promise.nextTick 去調(diào)度 resolce/reject 回調(diào),Q.resolve().then 在 process.nextTick 回調(diào)成功之前被調(diào)度進 nextTick 隊列中。我們可以減少代碼來看一下上面的程序的真實輸出,如下:

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

tips: 注意及時我在上面的例子中只使用 promise resolve 處理函數(shù),這個行為同樣適用于 promise reject 處理函數(shù)。最文章的最后,我將給出一個同時包含 resolve 和 reject 的輸出。

Bluebird 給了我們一個選擇,我們可以選擇自己調(diào)度編排。做這些意味著我們可以使用 process.nextTick 而不是 setImmediate 嗎?是的。Bluebird 提供一個 API 方法叫做 setScheduler,它可以獲取一個函數(shù)去修改默認的 setImmediate 調(diào)度。

使用 process.nextTick 作為 bluebird 的調(diào)度者,你可以這樣修改:

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

使用 setTimeout 作為 bluebird 的調(diào)度者你可以這樣寫:

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

tips: 為了避免一篇文章太長,我不會在這邊給出一個不同的 blurbird 調(diào)度的例子。你可以自己嘗試去使用一下。

使用 setImmediate 而不是 process.nextTick 在 node 最新的版本上使用是有一些優(yōu)點的。自從 NodeJS v0.12 及以上不提供 process.maxTickDepth 參數(shù),在事件循環(huán)中添加太多事件到 nextTick 隊列中會導致 I/O 饑餓。因此,如果在 node 最新版本上使用 setImmedita 而不是 process.nextTick 是安全的,因為如果沒有任何 nextTick 回調(diào),immediates 隊列會在 I/O 事件之后獲取執(zhí)行權,setImmediate 將永遠不會導致 I/O 饑餓。

最后一個轉(zhuǎn)折

如果你運行下面的程序,你將會發(fā)現(xiàn)一個令人費解的輸出:

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

現(xiàn)在你應該有兩個疑問?

1.如果 Q 在 promise 的 resolved/rejected 回調(diào)函數(shù)里面使用 process.nextTick,log 將會如何輸出呢?”q promise rejectd“ 在 ”next tick“ 之前。

2. 如果 Bluebird 在 Promise resolved/rejected 回調(diào)函數(shù)中使用 setImmediate,這邊又會如何輸出呢?”bluebird promise rejected“ 會在 ”set immediate“ 之前。

這是因為兩個庫在內(nèi)部對數(shù)據(jù)結構中的 promise resolved/rejected 進行排隊,并且使用 process.nextTick 或者 setImmediate 去處理隊列。

現(xiàn)在你知道了很多關于 setTimeout, setImmediate, process.nextTick 以及 promises,你應該對上面給的例子有很清晰的理解。下一篇文章中,我將詳細討論如何用事件循環(huán)處理 I/O。

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

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 本文適用的讀者 本文寫給有一定Promise使用經(jīng)驗的人,如果你還沒有使用過Promise,這篇文章可能不適合你,...
    HZ充電大喵閱讀 7,444評論 6 19
  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,823評論 1 56
  • 本文作者就是我,簡書的microkof。如果您覺得本文對您的工作有意義,產(chǎn)生了不可估量的價值,那么請您不吝打賞我,...
    microkof閱讀 16,067評論 9 40
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎知識-event-loop。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,868評論 0 5
  • 歡迎回到 Event Loop 系列文章!在第一篇文章中,我描述了關于 NodeJS 的一個整體情況。在這篇文章中...
    吃檸檬的刺猬閱讀 648評論 0 1

友情鏈接更多精彩內(nèi)容