天天看點

Promise和setTimeout執行順序 面試題

看到過下面這樣一道題:

(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

 ?

比較難回答,但我們可以首先說一說可以從輸出結果反推出的結論:

  1. Promise.then

     是異步執行的,而建立Promise執行個體( 

    executor

     )是同步執行的。
  2. setTimeout

     的異步和 

    Promise.then

     的異步看起來 “不太一樣” ——至少是不在同一個隊列中。

相關規範摘錄

在解答問題前,我們必須先去了解相關的知識。(這部分相當枯燥,想看結論的同學可以跳到最後即可。)

Promise/A+

 規範

要想找到原因,最自然的做法就是去看規範。我們首先去看看 Promise的規範 。

摘錄 

promise.then

 相關的部分如下:

promise.then(onFulfilled, onRejected)

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

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

     。

Promise和setTimeout執行順序 面試題

标簽:直接   ofo   ons   with   can   執行個體化   實作   macro   rpo   

原文位址:https://www.cnblogs.com/yzhihao/p/9383822.html

繼續閱讀