JavaScript - Macrotask 與 MicroTask 差異以及情境使用

提到 Macrotask 與 MicroTask 之前,先再次複習 Promise 的觀念:

  1. 一個 Promise 最終會有兩種狀態
  2. 對應 Promise 的不同狀態,會各自觸發 .then 與 .catch 兩個函式
  3. 利用 Promise 可以達成非同步行為,而且內容可以自訂

Macrotasks

關於 Event Queue 有些 Web API 是具有非同步的行為,而在非同步的目的達成之後,瀏覽器會把給定的對應的函式推送到 Event Queue 裡面,這些一個一個函式正好代表每一件要做的事情,因此在 JS 裡面,以「 Task 」或 「Macrotask 」來稱呼。

![Macrotask 執行情境](d1dwq032kyr03c.cloudfront.net/upload/images.. "Macrotask 執行情境")

關於 Task 有兩個細節可以注意:

  • 以瀏覽器的角度來看,在每一個 Task 結束之前,不會有任何瀏覽器的 rending 產生
  • 如果一個 Task 執行所花的時間過長,那麼瀏覽器就無法執行其他的 Task ,所以過一段時間之後會提出「頁面沒有回應」的警告,建議你關閉這個分頁,這種情況你應該有遇過。

Microtasks

Microtask 通常由 Promise 產生,Promise 裡用到的 .then / .catch 函式會以非同步的方式來被執行,回想下 Queue 的概念,非同步行為指的是,會在全域執行環境執行完之後才被執行,因此只要 Promise 的 callback 內容執行完成,狀態再也不是 pending 時,.then 或 .catch 的函式內容就會被推送到 Queue 裡面等待執行,這個被推送到 Queue 的函式就是 Microtask。

相對於管理 Web API 所屬事件的 Macrotask Queue ,Promise 產生的 Microtask 也有自己的 Queue ,在 JS 內被稱為 Job Queue 或 Microtask Queue,而 Job Queue 與 Event Queue 運作方式上有一點不一樣。

差在哪裡呢?在 Queue 裡面的每個 Macrotask 執行完畢後 ,就算 Event Queue 裡面還有其他的 Task,JS 引擎依舊會優先執行 Microtask Queue 裡面的所有 Task ,在這個同時也不會重新渲染網頁,換句話說,Microtask 的執行是穿插在每個 Macrotask 之間,兩者的差異也就在執行順序的不同而已。

![MicroTask 執行情境](d1dwq032kyr03c.cloudfront.net/upload/images.. "MicroTask 執行情境")

Microtask 與 Macrotask 同時發生的範例

前端面試經常會考到這題,這邊使用過去被考到的範例來做拆解解析

setTimeout(() => alert("timeout"));

Promise.resolve()
  .then(() => alert("promise"));

console.log("Global");

這段程式碼剛好同時用到 Web API 與 Promise ,各自在呼叫後會產生一個 Macrotask 以及 Microtask ,不過在順序上是哪個會先被執行呢?

  • 所有的 Queue 都會等待執行環境堆疊被清空,alert 肯定會先執行
  • setTimeout 對應的函式會被當作一個 Macrotask ,等待時間到之後被送入 Macrotask Queue
  • Promise 對應的 .then 或 .catch 的函式會被當作一個 Microtask 送入 Microtask Queue
  • 在執行環境堆疊清空之後,通常網頁會先做一次 Render,Render 的動作同時也算是一個 Macrotask

解析內容: 總共執行了三組程式碼,內容有 consolesetTimeoutPromise ,而 setTimeoutPromise第一個時間觀看到就會知道會產生 Event Loop 的情形,自動地將他的執行序往後排,所以優先會先去執行 console

  1. "Global"

接下來我們直接透過 JS 由上執行到下的觀念來判斷到底先執行 setTimeout 還是 Promise

先將答案推測為這樣:

  1. "Global"
  2. "timeout"
  3. "promise"

但是並不是!結果會是 "promise""timeout" 還要更先被 log 出來:

  1. "Global"
  2. "promise"
  3. "timeout"

這是為什麼呢?這邊可能會有點抽象,前面我們在分析 JS 語法與運作模式的時候,大多是從 JS 引擎的角度出發。而前面也有提到, Queue 的概念並不屬於 JS 引擎的一部分,相對的歸屬於瀏覽器。對於瀏覽器來說,在網頁頁面開啟時,載入對應的 JS 檔並且執行這件事情,也是一個 Macrotask 。

而剛剛提到 Macrotask 執行完畢後,會優先執行 Microtask ,因此你會看到 "promise" 出現的順序先於 "timeout"

![最後執行的結果](d1dwq032kyr03c.cloudfront.net/upload/images.. "最後執行的結果")