Event Loop

fefe發表於2019-12-30

Event loop

Javascript 基於 Event Loop 的執行方式與眾不同,microtask queue、task queue 讓很多人迷惑程式碼為什麼這樣執行。這裡就來看一眼 Event Loop。

Event loop 的執行過程

ECMA-262

作為 Javascript 背後的標準,ECMA-262 裡沒有定義 Event Lopp ,沒有 microtask queue 與 task queue 。(ECMA-262 同樣也沒有定義 setTimeout 等定時函式,也沒有 I/O。)

它定義了 Job 與 Job queue ,並有一個頂層的 [RunJobs] 操作:

  1. Perform ? InitializeHostDefinedRealm().
  2. In an implementation-dependent manner, obtain the ECMAScript source texts (see clause 10) and any associated host-defined values for zero or more ECMAScript scripts and/or ECMAScript modules. For each such sourceText and hostDefined, do

    1. If sourceText is the source code of a script, then
      i. Perform EnqueueJob("ScriptJobs", ScriptEvaluationJob, « sourceText, hostDefined »).
    2. Else sourceText is the source code of a module,
      ii. Perform EnqueueJob("ScriptJobs", TopLevelModuleEvaluationJob, « sourceText, hostDefined »).
  3. Repeat,

    1. Suspend the running execution context and remove it from the execution context stack.
    2. Assert: The execution context stack is now empty.
    3. Let nextQueue be a non-empty Job Queue chosen in an implementation-defined manner. If all Job Queues are empty, the result is implementation-defined.
    4. Let nextPending be the PendingJob record at the front of nextQueue. Remove that record from nextQueue.
    5. Let newContext be a new execution context.
    6. Set newContext's Function to null.
    7. Set newContext's Realm to nextPending.[[Realm]].
    8. Set newContext's ScriptOrModule to nextPending.[[ScriptOrModule]].
    9. Push newContext onto the execution context stack; newContext is now the running execution context.
    10. Perform any implementation or host environment defined job initialization using nextPending.
    11. Let result be the result of performing the abstract operation named by nextPending.[[Job]] using the elements of nextPending.[[Arguments]] as its arguments.
    12. If result is an abrupt completion, perform HostReportErrors(« result.[[Value]] »).

這裡,首先根據指令碼的型別,將 ScriptEvaluationJobTopLevelModuleEvaluationJob 加入 ScriptJob 佇列。然後開始迴圈處理每一個非空 Job queue 。

在同一個 Job queue 內部,任務是嚴格先進先出的。但是,選擇哪一個是實現決定的。然而,ECMA-262 僅定義了兩個 Job queue ,一個是上面的頂層的 ScriptJob ,只用於放(唯一一個)頂層任務;另一個是 PromiseJob ,用於 Promise 。這兩個 Job queue 不會同時非空,因而執行順序其實是確定的。

EnqueuJob被用於想 Job queue 加入 Job。

HTML

在 HTML 中使用的 Javascript,並沒有使用上面提到的 RunJobs ,以及 Job queue ,而是定義了自己的 Event Loop ,以及 task queuemicrotask queue

HTML 的 Event Loop 執行如下的操作

  1. Let taskQueue be one of the event loop's task queues, chosen in a user-agent-defined manner, with the constraint that the chosen task queue must contain at least one runnable task. If there is no such task queue, then jump to the microtasks step below.
  2. Let oldestTask be the first runnable task in taskQueue, and remove it from taskQueue.
  3. Report the duration of time during which the user agent does not execute this loop by performing the following steps:

    1. Set event loop begin to the current high resolution time.
    2. If event loop end is set, then let top-level browsing contexts be the set of all top-level browsing contexts of all Document objects associated with the event loop. Report long tasks, passing in event loop end, event loop begin, and top-level browsing contexts.
  4. Set the event loop's currently running task to oldestTask.
  5. Perform oldestTask's steps.
  6. Set the event loop's currently running task back to null.
  7. Remove oldestTask from its task queue.
  8. Microtasks: Perform a microtask checkpoint.
  9. Let now be the current high resolution time. [HRT]
  10. Report the task's duration by performing the following steps:

    1. ...
  11. 以下省略

注意,microtask queue 不是 task queue。task queue 不是佇列,因為它不是先進先出的。從第一步可以看出從 task queue 中取出的任務並不一定是最先進入 task queue 的任務。

在 Event Loop 中,完成一個 task queue 的任務之後,會Perform a microtask checkpoint :

  1. If the event loop's performing a microtask checkpoint is true, then return.
  2. Set the event loop's performing a microtask checkpoint to true.
  3. While the event loop's microtask queue is not empty:

    1. Let oldestMicrotask be the result of dequeuing from the event loop's microtask queue.
    2. Set the event loop's currently running task to oldestMicrotask.
    3. Run oldestMicrotask.
    4. Set the event loop's currently running task back to null.
  4. For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object.
  5. Cleanup Indexed Database transactions.
  6. Set the event loop's performing a microtask checkpoint to false.

在這裡,將會按照先進先出的順序,執行所有 microtask queue 中的任務(包括在此過程中新進入 microtask queue 的任務), 直到 microtask queue 為空。

所以執行過程是,執行一個 task queue 的任務,然後執行 microtask queue 中的所有任務,然後進入 Event Loop 下一個迴圈,再執行一個 task queue 中的任務。

ECMA-262 中由 EnqueuJob 加入的 Job ,在 HMTL 中全部進入 microtask queue).

如果僅有由 Promise 生成的 microtask 的話,上述的執行過程與 RunJobs 基本是一致的。所以以後的討論,都基於 HTML 的 Event Loop 與 microtask / task 的定義進行。

Task vs Microtask

那麼,microtask 跟 task 都各有那些呢? 這裡挑一部分來介紹,應該可以解決很多網上的“為什麼輸出會是這樣”的疑問。

Microtask

  1. 所有 ECMA-262 中的 Promise 相關的 Job。包括:

    1. thencatch 的回撥

      • 如果 Promise 在呼叫 thencatch 時已經 settle(狀態已確定),那麼相應的回撥函式直接加入 microtask queu,參見 PerformPromiseThen 。否則,回到被記錄,並在 Promise settle 時,通過TriggerPromiseActions 加入 micro task queue。
      • await 是由 Promise 實現的,每一個 await ,都會通過 Promise.then 執行 await 結束之後的操作(即使 await 的物件不是 Promise)。(參見Await
    2. 由一個 Promise (P1) 去 resolve 另一個 Promise (P2)的時候,自動生成的一個對 P2.then 的呼叫。該呼叫會被加入 microtask queue 。

      • 參見 Promise Resolve Functions
      • P2.then 執行後,會使 P2.then 的回撥成為另一個 microtask
      • P2.then 的回撥,將 resolve 或 reject P1
  2. Mutation Observers

Task

  1. SetTimeoutSetInterval 的回撥。即使時間設定為 0

相關文章