Promise的隊列與setTimeout的隊列有何關聯?

setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
for( var i=0 ; i&<10000 ; i++ ){ i==9999 resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3);

為什麼結果是:

1,2,3,5,4

而不是:

1,2,3,4,5

Promise維護的隊列和通常說的「事件循環(Event loop)」的隊列有何關聯?只知道共同點是都是等同步代碼執行完畢後再執行。


先把問題貼一下:

Promise的隊列與setTimeout的隊列的有何關聯?

setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i&<10000 ; i++ ){ i==9999 resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3);

為什麼結果是:

1,2,3,5,4

而不是:

1,2,3,4,5

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

不想看故事的,直接看結論在這。

原因:

有一個事件循環,但是任務隊列可以有多個

整個script代碼,放在了macrotask queue中,setTimeout也放入macrotask queue。

但是,promise.then放到了另一個任務隊列microtask queue中。

這兩個任務隊列執行順序如下,取1個macrotask queue中的task,執行之。

然後把所有microtask queue順序執行完,再取macrotask queue中的下一個任務。

代碼開始執行時,所有這些代碼在macrotask
queue中,取出來執行之。

後面遇到了setTimeout,又加入到macrotask queue中,

然後,遇到了promise.then,放入到了另一個隊列microtask queue

等整個execution context
stack執行完後,

下一步該取的是microtask queue中的任務了。

因此promise.then的回調比setTimeout先執行。

(下面,多圖,多坑,多水。。。危險。。。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

本來我是這樣以為的:

因為promise.then既可以在pending的時候註冊回調,也可以在fullfill狀態註冊回調。

在pending的時候,註冊的是非同步回調。而在fullfill狀態,註冊的是同步回調。

只有非同步回調才會依賴任務隊列,而同步回調馬上執行。

題主這種情況,註冊的是同步回調。

注意,new Promise是同步的,會馬上執行function參數中的事情。

等function參數執行完,new Promise才返回一個promise實例對象。

這時候再調用then,其實是已經fullfill了。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

然而,我錯了。

感謝 @richard 指正。

因為,如果promise.then註冊的是同步回調的話,5應該比3先執行。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

那好吧,從零開始了,重新分析問題

老老實實翻開規範《ECMAScript? 2015 Language Specification》,搜setTimeout。

納尼?沒有。

想起來了,這是window對象的東西,跟瀏覽器宿主環境有關,不屬於EcmaScript範圍。

那怎麼搞,每個瀏覽器難道自己隨便實現嗎?谷歌了一把,找到了這個:

javascript - setTimeout(): If not defined in EcmaScript spec, where can I learn how it works?

原來HTML5規範中還是有介紹的。6.4 Timers

查了查setTimeout,似乎沒什麼收穫,那再看看HTML的事件循環吧。

6.1.4 Event loops

一個瀏覽器環境,只能有一個事件循環

而一個事件循環可以多個任務隊列

關於任務源:

它指出,一個瀏覽器環境(unit of related similar-origin browsing contexts.)只能有一個事件循環(Event loop),而一個事件循環可以多個任務隊列(Task queue),每個任務都有一個任務源(Task source)。

相同任務源的任務,只能放到一個任務隊列中。

不同任務源的任務,可以放到不同任務隊列中。

(同一個任務隊列,能否容納不同任務源的任務,沒說)

又舉了一個例子說,客戶端可能實現了一個包含滑鼠鍵盤事件的任務隊列,還有其他的任務隊列,而給滑鼠鍵盤事件的任務隊列更高優先順序,例如75%的可能性執行它。這樣就能保證流暢的交互性,而且別的任務也能執行到了。但是,同一個任務隊列中的任務必須按先進先出的順序執行。

結論:可以有多個任務隊列,目的想必是方便調整優先順序吧。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

回到EcmaScript規範上來,

76頁 8.4 Jobs and Job Queues

&它指出,任務隊列(Job queue)是一個先進先出的隊列,每一個任務隊列是有名字的,至於有多少個任務隊列,取決於實現。每一個實現至少應該包含以上兩個任務隊列。

它指出,任務隊列(Job queue)是一個先進先出的隊列,每一個任務隊列是有名字的,至於有多少個任務隊列,取決於實現。每一個實現至少應該包含以上兩個任務隊列。

以下又強調了,單獨的任務隊列中的任務總是按先進先出的順序執行,但是不保證多個任務隊列中的任務優先順序,具體實現可能會交叉執行

哪裡用到這個任務隊列了呢,Promise就用了,492頁。

&題主的問題,屬於fulfilled的情況,如圖所示。

題主的問題,屬於fulfilled的情況,如圖所示。

會把一個任務放到名為「PromiseJobs」的任務隊列中。

結論:EcmaScript的Job queue與HTML的Task queue有異曲同工之妙。它們都可以有好幾個,多個任務隊列之間的順序都是不保證的。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

那為什麼setTimeout會後執行呢,可能是它所屬的任務隊列優先順序比較低吧。

我之前對這個問題產生了誤解,主要是規範研究的不仔細,以為任務隊列只有一個。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

更新一:

關於和tick的關係,群里的小夥伴們嘗試了各種辦法,在昵稱為「第七片魂器」大神的指點下,

我們先後用setImmediateprocess.nextTick進行了實驗。(node.js。。。

setImmediate(function(){ console.log(1); },0); setTimeout(function(){ console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); }); console.log(6); process.nextTick(function(){ console.log(7); }); console.log(8);

結果:3 4 6 8 7 5 2 1

事件的註冊順序如下:

setImmediate - setTimeout - promise.then - process.nextTick

因此,我們得到了優先順序關係如下:

process.nextTick &> promise.then &> setTimeout &> setImmediate

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

更新二:

後來討論進入了白熱化,大神「東方(成都-潛水豬)」提到了promsieA+規範

圖靈社區 : 閱讀 : 【翻譯】Promises/A+規範

然而,後面的譯者注有問題,與前面的優先順序實驗不符。

process.nextTick &> promise.then &> setTimeout &> setImmediate

&又翻到了原版:

又翻到了原版:https://promisesaplus.com/

&原版的含義,有種依賴於實現的意思,隨便玩。。。

原版的含義,有種依賴於實現的意思,隨便玩。。。

而且macro-task和micro-task到底包含哪些也沒詳細說。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

更新三:

然後,重新看了遍聊天記錄,原來「-超亼/夿夿(廣州—堅殼)」大神已經指出過了。。。

又翻到了漢語版promsieA+規範中引的那篇文章

文章鏈接:Promise進階介紹+原生實現

一切都明朗了

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

更新四:

其實大神「最愛檸檬(南京-菜B檬檬)」一開始就貼了V8源碼。。。

只是吾等平民,實在太銼了。。。

(眼下,正在被虐中。。。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

感悟1:人丑就該多讀書。

感悟2:你永遠都沒有新的想法,除非在寫博士論文。

感悟3:我不生產答案,我只是大神們聊天記錄的搬運工。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

(先發這些,以後有新結論再更新。。。

剩下的疑問:

1. process.nextTick也會放入microtask quque,為什麼優先順序比promise.then高呢?

2. 到底setTimeout有沒有一個依賴實現的最小延遲?4ms?

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2017.02.25更新

剩下的疑問,第一個問題,已經被評論區,大神 @黃一君 指出,以下是原文,

「process.nextTick 永遠大於 promise.then,原因其實很簡單。。。在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被調用,而這個_tickCallback中實質上幹了兩件事:

1. nextTickQueue中所有任務執行掉(長度最大1e4,Node版本v6.9.1)

2.第一步執行完後執行_runMicrotasks函數,執行microtask中的部分(promise.then註冊的回調)

所以很明顯process.nextTick &> promise.then」

第二個問題,也由評論區,大神 @魯小夫 指出,

「4ms已經標準化了」

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

2017.03.08更新

上文為了強調任務隊列可以有多個是這樣說的:

「有一個事件循環,但是任務隊列可以有多個。」

感謝知友 @Jschyz 指出錯誤

&> 根據2017年新版的HTML規範,瀏覽器包含2類事件循環:browsing contexts 和 web workers。 鏈接:HTML Standard

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

&> Today is better than yersterday.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

QQ群:JS高級前端開發 159758989

總入口:JS前端開發跳板群 492107297


瀉藥

真的不會Promise神馬的

貌似這玩意node能用

firefox 的最新版能用

node 偶也不會……

加班呢

反正不會+沒仔細研究過這個

所以胡扯扯

答錯不負責任啊……

V8和規範

其實這哥倆跟這事兒都沒關係

它們都不定義宿主實現

也就說他們都沒有MessageLoop(EventLoop)的任何具體實現和說明

只是一個抽象概念

Promise這玩意的callback時機存粹是留給宿主環境實現的

由於Rejected得非同步

所以看 RejectEvent 相關的

比如說node

node/node.cc at c214ab0f79ba891156a5bcb82b42a67f8b3ac4a2 · nodejs/node · GitHub

node/node.js at af46112828cb28223050597f06b5e45a659e99d6 · nodejs/node · GitHub

它是使用 nextTick 檢測任務情況以及執行then

nextTick 又比 timeout 執行時機早

也就是 then 必然會先執行

當然估計firefox、chromium也是如此實現

下班回家

ps:

在node里

由於RejectEvent後包裝的

代碼中寫死的nextTick總會先一步在next數組中push任務

所以總是 process.nextTick &> promise.then

====== 補充碎碎念 ======

剛覺得這玩意是不是從http://d8.cc 里能看到點確切什麼信息

因為如果 v8 沒支持 promise 自驅動

d8 里promise 應該也跑不動

(d8 v8代碼的乾淨shell環境)

結果是然並卵

FunctionTemplate 會給個 isolate

isolate 會接收 WorkerNew

而 WorkerNew 最終 worker-&>StartExecuteInThread(*script)

StartExecuteInThread 就是 new WorkerThread(this);

所以最終 d8 還是整線程給 isolate 了

它的實現 v8/isolate.cc at 19bad2a68c1a40de7db75a85f61ccc74c5e81eb8 · v8/v8 · GitHub

里n多與promise 棧和 Thread 相關

看起來還是 promise 依賴外部處理來實現相關回調

V8 本身沒提供任何自loop供 promise 自驅動

依然是以上猜測

除非有時間跟下promise的流程了


這個沒啥糾結的,因為 setTimeout 是屬於 macrotask 的,而整個 script 也是屬於一個 macrotask, promise.then 回調 是 microtask ,執行過程大概如下:

  • 由於整個 script 也屬於一個 macrotask, 由於會先執行 macrotask 中的第一個任務,再加上promise 構造函數因為是同步的,所以會先列印出 1和2
  • 然後繼續同步執行末尾的 console.log(3) 列印出3
  • 此時 setTimeout 被推進到 macrotask 隊列中, promise.then 回調被推進到 microtask 隊列中
  • 由於在第一步中已經執行完了第一個 macrotask , 所以接下來會順序執行所有的 microtask, 也就是 promise.then 的回調函數,從而列印出5
  • microtask 隊列中的任務已經執行完畢,繼續執行剩下的 macrotask 隊列中的任務,也就是 setTimeout, 所以列印出 4


好,我來了。

像這種事情,看標準是最容易的了。

首先,new Promise(fn);var x=1;這裡面的fn是同步執行的(也就是說在當前event loop里立即執行的[早於var x=1這句的執行]。這個我在標準裡面沒有找到,但是很容易用實際代碼試出來,所有的實現也都是這樣的行為),所以在題目中的程序中,1和2肯定是最先列印出來的。

然後,標準說了

onFulfilled and onRejected must be called asynchronously;

https://promisesaplus.com/differences-from-promises-a 3.3節

所以,你知道了,.then(fn)里的這個fn肯定是非同步執行的(也就是說最快也要到下一個event loop),而第一行的`setTimeout(function(){console.log(4)},0);`在瀏覽器則是在(注意跟前面的「最快也要到」的區別)下一個event loop里執行的。現在的問題就在於,.then是使用何種方式調用fn的,如果也是setTimeout(0),那麼5肯定是要晚於4出現,所以如果你用我實現的xieranmaya/Promise3 · GitHub來運行這段代碼的話,5應該是晚於4出現的,因為我的implementation里then是使用setTimeout(0)來調用fn的。

然而很多Promise的實現在內部並不是用setTimeout來調用then里的回調的,比如在Node里使用的是process.nextTick或者setImmediate(avow/avow.js at master · briancavalier/avow · GitHub)。在瀏覽器里自己實現一個「性能」更高的類似setTimeout(0)原因是瀏覽器和Node里的setTimeout實現都有一定程度的延遲(https://gist.github.com/mmalecki/1257394),經人測試出來是4ms(http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon),別小看這4ms,長點的Promise鏈式調用一不小心就十幾二十個4ms延遲了,這麼下來就有可能讓ui有比較明顯的延遲,前面一個鏈接里實現的soon函數,是把所有傳入的函數存起來,等到有機會執行時一次性執行完,雖然在內部也是調用的setTimeout(0)但延遲就只有一個4ms了,Q的內部也實現了一個叫做nextTick的函數(q/q.js at v1 · kriskowal/q · GitHub),原理應該跟soon差不太多,有興趣可以自行看看代碼。

題主這段代碼輸出 12354 的話,應該是使用ES6原生Promise來執行的。

當初我實現Promise的時候就發現瀏覽器(至少是Chrome)里已經某種程度上實現了類似process.nextTick或者setImmediate的函數。

參見下面兩張圖,我就不細解釋了(前為我的實現,後為原生,注意列印出undefined的時機):

所以說這裡輸出12354的原因就是因為then在調用裡面回調的時候使用了「優先順序」比setTimeout(0)更高的方法,所以5才會早於4列印出來。

樓主可以試試在不同瀏覽器里用不同的Promise實現來運行一下這段代碼,在老版本的瀏覽器里(IE8),應該所有的實現都會輸出12345,因為在IE8里沒辦法實現一個性能更高的setTimeout(在單次調用的情況下,多次的話還是可以把函數存下來後面用循環同步執行)

Node原生就有nextTick和setImmediate,但是在測試中我發現setTimeout(0)在Node里比setImmediate執行時機更早,所以具體還是要看實現是用的哪一個函數,這裡就不展開討論了。

以上。


補充下排名第一的答案最後的問題:

「剩下的疑問: process.nextTick也會放入microtask quque,為什麼優先順序比promise.then高呢? 到底是不是setTimeout有一個依賴實現的最小延遲?4ms?」

process.nextTick 永遠大於 promise.then,原因其實很簡單。。。在Node中,_tickCallback在每一次執行完TaskQueue中的一個任務後被調用,而這個_tickCallback中實質上幹了兩件事:
1. nextTickQueue中所有任務執行掉(長度最大1e4,Node版本v6.9.1)
2. 第一步執行完後執行_runMicrotasks函數,執行microtask中的部分(promise.then註冊的回調)
所以很明顯process.nextTick &> promise.then


我這篇文章有代碼,有語音,保證你理解

大話前端之NodeJS中的Event Loop


then 中的 resolve 和 rejector 會被包在一個 setTimeout 中,為了確保運行時你的 handler 處於一個 "乾淨的" 執行棧中。

你可以理解為:

promise.then = function(resolver, rejector) {
switch(this.state) {
case "resolved":
setTimeout(resolver, 0);
break;
}
};


我補充一下,es6-promise起作用時的順序是 1 2 3 4 5


額...如果這個promise是jquery版本的....應該會是15234吧.....


哈哈哈,這個問題真是把 Javascript/Node.js 最 fucked up 的一面展現了出來,你永遠不知道一個 callback 會被同步調用還是非同步調用。。而且 API 也沒有區分非同步和同步,如果我沒記錯 `stream` 的代碼注釋里就有對需要同時兼容同步和非同步回調的吐槽。。

所以最好的辦法就是在代碼里不依賴它們之間的順序關係,具體是 `setTimeout` 還是 `onFullfilled` 先執行又有什麼關係呢?為什麼要把兩條有順序依賴的語句分別放在不同的函數回調里。。

這裡我認為比較有意義的是 2 和 5 直接的順序,也就是 `onFullfilled` 回調究竟是同步還是非同步,不看 Promise/A+ 的 spec,自己思考一下 `Promise` 的實現,好像非同步調用實現起來比較乾淨一點。。

而且,`EventEmitter` 回調用的是同步,應該有不少人最開始寫 Node 代碼時被坑過。。


12354是標準的結果。

首先我們看看能不能改一下你的說法。「共同點是都是等同步代碼執行完畢後再執行」,改為「共同點是都在event loop的當前tick執行完畢後再執行」,這麼說會更恰當些。

那麼setTimeout( callback, 0 )的意思是,在event loop的下一個tick中執行callback。

promise.then( callback ),不論是本身的需要,還是ECMAScript6中的規定(Jop Queue),都會讓callback在本次tick結束後立即執行,先於下一個tick,執行也不在原本event loop的ticks中。


setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);

結果是:3 4 6 8 7 5 2 1

優先順序關係如下:

process.nextTick &> promise.then &> setTimeout &> setImmediate

V8實現中,兩個隊列各包含不同的任務:

macrotasks: script(整體代碼),setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe, MutationObserver

執行過程如下:
JavaScript引擎首先從macrotask queue中取出第一個任務
執行完畢後,將microtask queue中的所有任務取出,按順序全部執行
然後再從macrotask queue中取下一個
執行完畢後,再次將microtask queue中的全部取出;
循環往複,直到兩個queue中的任務都取完。

解釋:
代碼開始執行時,所有這些代碼在macrotask queue中,取出來執行之。
後面遇到了setTimeout,又加入到macrotask queue中,
然後,遇到了promise.then,放入到了另一個隊列microtask queue
等整個execution context stack執行完後,
下一步該取的是microtask queue中的任務了。
因此promise.then的回調比setTimeout先執行。


這個文章是否有幫助

Promise源碼分析(二) - 知乎專欄


setTimeout 必須在代碼都執行完了才開始計時


給幾個keyword:

google,eventloop,macrotask,microtask,webapi


從Vue.js源碼看nextTick機制

JS 的 event loop 執行時會區分 task 和 microtask,引擎在每個 task 執行完畢,從隊列中取下一個 task 來執行之前,會先執行完所有 microtask 隊列中的 microtask。

setTimeout 回調會被分配到一個新的 task 中執行,而 Promise 的 resolver、MutationObserver 的回調都會被安排到一個新的 microtask 中執行,會比 setTimeout 產生的 task 先執行。

要創建一個新的 microtask,優先使用 Promise,如果瀏覽器不支持,再嘗試 MutationObserver。

實在不行,只能用 setTimeout 創建 task 了。

為啥要用 microtask?

根據 HTML Standard,在每個 task 運行完以後,UI 都會重渲染,那麼在 microtask 中就完成數據更新,當前 task 結束就可以得到最新的 UI 了。

反之如果新建一個 task 來做數據更新,那麼渲染就會進行兩次。

setTimeout會默認延遲4ms。

setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
for( var i=0 ; i&<10000 ; i++ ){ i==9999 resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3); 結果是: 1,2,3,5,4


setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i&<10000 ; i++ ){ i==9999 resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3);

setTimeout(f,0)時候是定時器,放入隊列。

new Promise時候,

Promise是一個構造函數,裡面立即執行了,此構造傳入的參數是一個函數,也在內部執行了,於是 console.log(1)。

接著發現此處有一個大循環,無法判斷自己的狀態,於是pending(而且處於死循環狀態),在循環結束後,console.log(2)是最先出現的語句。

接著發現其他的.then以及setTimeout都是非同步。放在隊列裡面。promise.then放在了setTimeout(f,0)之前(此處是V8自己的實現)。

發現console.log(3)是同步代碼,先執行完。

開始執行非同步代碼。

console.log(5)

開始執行setTimeout部分的非同步代碼

console.log(4)


Job 是 ES6 中新增的概念,它與 Promise 的執行有關,可以理解為等待執行的任務;Job Queue 就是這種類型的任務的隊列。JavaScript Runtime 對於 Job Queue 與 Event Loop Queue 的處理有所不同。

相同點:

  • 都用作先進先出隊列

相異點:

  • 每個 JavaScript Runtime 可以有多個 Job Queue,但只有一個 Event Loop Queue
  • 當 JavaScript Engine 處理完當前 chunk 後,優先執行所有的 Job Queue,然後再處理 Event Loop Queue

ES6 中,一個 Promise 就是一個 PromiseJob,一種 Job。


雖然都是事件隊列,但是還是有區別的。

宏隊列每執行一個,微隊列就要全部清空……好像是這麼說的。


原因是使用 Promise.then 添加的回調函數會被放到 microtask 隊列中,而 setTimeout 的回調函數會在 task 隊列,或者說 macrotask 。

事件循環會先取 microtask 的任務,直到清空。

而 task 隊列則每次只取一個任務,之後會再次清空 microtask 隊列,再取一個 task 。


推薦閱讀:

大廠前端對演算法的要求如何?
如果dom結構很深 如何使less寫的更加優雅 層級不會過多
為什麼現在又流行服務端渲染html?
完全理解jQuery源代碼,在前端行業算什麼水平?
作為一個剛入門的前端愛好者,以後立志成為前端攻城獅的我,應該要學習哪些方面的知識?

TAG:前端開發 | ECMAScript2015 |