Promise和setTimeout執行順序 面試題

看到過下面這樣一道題:html

(function test() {
    setTimeout(function() {console.log(4)}, 0); new Promise(function executor(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 ?html5

比較難回答,但咱們能夠首先說一說能夠從輸出結果反推出的結論:web

  1. Promise.then 是異步執行的,而建立Promise實例( executor )是同步執行的。
  2. setTimeout 的異步和 Promise.then 的異步看起來 「不太同樣」 ——至少是不在同一個隊列中。

相關規範摘錄

在解答問題前,咱們必須先去了解相關的知識。(這部分至關枯燥,想看結論的同窗能夠跳到最後便可。)windows

Promise/A+ 規範api

要想找到緣由,最天然的作法就是去看規範。咱們首先去看看 Promise的規範 。promise

摘錄 promise.then 相關的部分以下:瀏覽器

promise.then(onFulfilled, onRejected)app

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].webapp

Here 「platform code」 means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a 「macro-task」 mechanism such as setTimeout or setImmediate, or with a 「micro-task」 mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or 「trampoline」 in which the handlers are called.異步

規範要求, onFulfilled 必須在 執行上下文棧(execution context stack) 只包含 平臺代碼(platform code) 後才能執行。平臺代碼指 引擎,環境,Promise實現代碼。實踐上來講,這個要求保證了 onFulfilled 的異步執行(以全新的棧),在 then 被調用的這個事件循環以後。

規範的實現能夠經過 macro-task 機制,好比 setTimeout 和 setImmediate ,或者 micro-task 機制,好比 MutationObserver 或者 process.nextTick 。由於promise的實現被認爲是平臺代碼,因此能夠本身包涵一個 task-scheduling 隊列或者 trampoline 。

經過對規範的翻譯和解讀,咱們能夠肯定的是 promise.then 是異步的,但它的實現又是平臺相關的。要繼續解答咱們的疑問,必須理解下面幾個概念:

  1. Event Loop,應該算是一個前置的概念,理解它才能理解瀏覽器的異步工做流程。
  2. macro-task 機制和 micro-task 機制,這組概念很新,以前根本沒聽過,但倒是解決問題的核心。

Event Loop 規範

HTML5 規範裏有 Event loops 這一章節(讀起來比較晦澀,只關注相關部分便可)。

  1. 每一個瀏覽器環境,至多有一個event loop。
  2. 一個event loop能夠有1個或多個task queue。
  3. 一個task queue是一列有序的task,用來作如下工做: Events task, Parsing task, Callbacks task, Using a resource task, Reacting to DOM manipulation task等。

每一個task都有本身相關的document,好比一個task在某個element的上下文中進入隊列,那麼它的document就是這個element的document。

每一個task定義時都有一個task source,從同一個task source來的task必須放到同一個task queue,從不一樣源來的則被添加到不一樣隊列。

每一個(task source對應的)task queue都保證本身隊列的先進先出的執行順序,但event loop的每一個turn,是由瀏覽器決定從哪一個task source挑選task。這容許瀏覽器爲不一樣的task source設置不一樣的優先級,好比爲用戶交互設置更高優先級來使用戶感受流暢。

Jobs and Job Queues 規範

原本應該接着上面Event Loop的話題繼續深刻,講macro-task和micro-task,但先不急,咱們跳到 ES2015 規範,看看 Jobs and Job Queues 這一新增的概念,它有點相似於上面提到的 task queue 。

一個 Job Queue 是一個先進先出的隊列。一個ECMAScript實現必須至少包含如下兩個 Job Queue :

Name Purpose
ScriptJobs Jobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15.
PromiseJobs Jobs that are responses to the settlement of a Promise (see 25.4).

單個 Job Queue 中的PendingJob老是按序(先進先出)執行,但多個 Job Queue 可能會交錯執行。

跟隨PromiseJobs到25.4章節,能夠看到 PerformPromiseThen ( promise, onFulfilled, onRejected, resultCapability ) :

這裏咱們看到, promise.then 的執行實際上是向 PromiseJobs 添加Job。

event loop怎麼處理tasks和microtasks?

好了,如今可讓咱們真正來深刻task(macro-task)和micro-task。

認真說,規範並無包括macro-task 和 micro-task這部分概念的描述,但閱讀一些大神的博文以及從規範相關概念推測,如下所提到的在我看來,是合理的解釋。可是請看文章的同窗辯證和批判地看。

首先, micro-task在ES2015規範中稱爲Job。 其次,macro-task代指task。

哇,因此咱們能夠結合前面的規範,來說一講Event Loop(事件循環)是怎麼來處理task和microtask的了。

  1. 每一個線程有本身的事件循環,因此每一個web worker有本身的,因此它才能夠獨立執行。然而,全部同屬一個origin的windows共享一個事件循環,因此它們能夠同步交流。
  2. 事件循環不間斷在跑,執行任何進入隊列的task。
  3. 一個事件循環能夠有多個task source,每一個task source保證本身的任務列表的執行順序,但由瀏覽器在(事件循環的)每輪中挑選某個task source的task。
  4. tasks are scheduled,因此瀏覽器能夠從內部到JS/DOM,保證動做按序發生。在tasks之間,瀏覽器可能會render updates。從鼠標點擊到事件回調須要schedule task,解析html,setTimeout這些都須要。
  5. microtasks are scheduled,常常是爲須要直接在當前腳本執行完後當即發生的事,好比async某些動做但沒必要承擔新開task的弊端。microtask queue在回調以後執行,只要沒有其它JS在執行中,而且在每一個task的結尾。microtask中添加的microtask也被添加到microtask queue的末尾並處理。microtask包括 mutation observer callbacks 和 promise callbacks 。

結論

定位到開頭的題目,流程以下:

  1. 當前task運行,執行代碼。首先 setTimeout 的callback被添加到tasks queue中;
  2. 實例化promise,輸出 1 ; promise resolved;輸出 2 ;
  3. promise.then 的callback被添加到microtasks queue中;
  4. 輸出 3 ;
  5. 已到當前task的end,執行microtasks,輸出 5 ;
  6. 執行下一個task,輸出 4 。
相關文章
相關標籤/搜索