所有文章

事件循環之 Macrotask & Microtask

定義

一個事件循環(event loop)中會有一個正在執行的任務(task),這個任務就是從 macrotask 中來的。當該 macrotask 執行結束後,所有可用的 microtask 將會在同一個事件循環中執行,當這些 microtask 執行結束後還能繼續添加 microtask 直至整個 microtask 隊列執行結束。

用法

當我們想以同步的方式處理異步任務時就用 microtask(比如需要直接在某斷代碼後去執行某個任務,正如 Promise)。其他情況就直接用 macrotask。

  • macrotasks: setTimeoutsetInterval、I/O、UI 渲染
  • microtasks: Promise、process.nextTickObject.observe、MutationObserver

規範

根據 WHATWG 規範:

  • 一個 event loop 會有一個或多個 task queue 即 macrotask queue
  • 每個 event loop 都有一個 microtask queue
  • task queue == macrotask queue != microtask queue
  • 一個 task 可放入 macrotask queue 亦可放入 microtask queue
  • 當一個 task 被放入 queue(macro/micro)則該 task 可被立即執行

當 call stack 為空時,開始依次執行:

  1. 把最早的任務 task A 放入任務隊列
  2. 若 task A 為 null(則任務隊列為空),直接跳到第 6 步
  3. 將 currently running task 設為 task A
  4. 執行 task A(即執行回調函式)
  5. 將 currently running task 設為 null 並移出 task A
  6. 執行 microtask queue

    1. 在 microtask 中選出最早的任務 task X
    2. 若 task X 為 null,直接跳到 xii
    3. 將 currently running task X 設為 task X
    4. 執行 task X
    5. 將 currently running task 設為 null 並移出 task X
    6. 在 microtask 中選出最早的任務,跳到 ii
    7. 結束 microtask queue
  7. 跳到第 1 步

上面是一個簡單的 event loop 執行模型。再簡單點可總結為:

  1. 在 macrotask queue 中執行最早的 task,然後移出
  2. 執行 microtask 隊列中所有可用的任務,然後移出
  3. 下一個循環,執行下一個 macrotask 中的任務(跳到第 2 步)

其他

  • 當一個 macrotask 正處執行狀態,亦可能會有新的事件被註冊(創建新 task)。比如:

    • promiseA.then() 的回調就是一個 task

      • promiseA 是 resolved 或 rejected,則該 task 會放入當前事件循環回合的 microtask queue
      • promiseA 是 pending,則該 task 會放入事件循環的未來某個回合的 microtask queue
    • setTimeout 的回調也是 task,它會被放入 macrotask queue(即使是 0ms)
  • microtask queue 中的 task 會在事件循環的當前回合中執行,因此 macrotask queue 中的 task 就只能等到事件循環的下一個回合執行
  • click、Ajax、setTimeout 的回調都是 task,包裹在一個 <script> 標籤中的 JS 代碼亦是一個 task——它們都是 macrotask。

完 :)

發佈於 2 Jun 2018

慎獨|純亦不已
Jason Lam on Twitter