How does Event Loop works: 宏任務(Macro Task) 和 微任務 (Micro Task)
Published at 2024-12-03
什麼是Event Loop?
讓非同步操作在不阻塞主執行緒的情況下執行,同時仍保持 JavaScript 是單執行緒的特性。 想像一個廚房,一名廚師(Call Stack) 一次只能處理一件事,但訂單(Task) 會不斷送進廚房,進入隊列(Task Queue)。有些料理可以馬上完成(同步函式),有些則需要花比較多時間(非同步函式),這時廚師可能會把這些料理送到烤箱或電鍋去處理(Web Apis) ,等到他有空閒時會處理下一張單。
Event Loop 的組成
-
Call Stack: LIFO (Last In First Out) 的堆疊,儲存當前被呼叫執行的同步函式。像是在疊盤子,最晚疊上來的會最先被抽走。 -
Task Queue (Callback Queue): 任務的序列,存放非同步函式的 callback。FIFO (First In First Out) 設計,先來的會先執行。 -
Web Apis: 由瀏覽器或Runtime(如Node.js) 提供,分為 callback-based 和 promise-based,callback-based 如 setTimeout, DOM操作。promise-based 如 fetch 發送請求。當web api 被呼叫時,實際的執行是在瀏覽器的背景線程中進行的,不會阻塞主執行緒。舉個例子,當 setTimeout被呼叫倒數100毫秒,實際上倒數的任務是 web api 自己的 thread去處理,而不是在 Task Queue處理。這些 web api 的 callback 函式會進到 Task Queue (也稱為宏任務)。 -
Micro Task Queue: 優先於 Task Queue 的任務序列,FIFO (First In First Out) 設計。當 Call Stack 空閒時,會優先檢查這裡有沒有待執行的任務(也稱為微任務)。會進入 Micro Task Queue 的有:Promise 的 callback ( .then, .catch, .finally )、 Async / Await 的 callback 、MutationObserver(會在 DOM 有變化時被呼叫)的callback、queueMicrotask(一個直接將callback 加入 Micro Task Queue 的 API) 的 callback。
簡易圖示:

宏任務(Macro Tasks)與微任務(Micro Tasks)的區分
JavaScript 的任務分為兩類:
- 宏任務(Macro Tasks):包括整個 script 的執行、
setTimeout,setInterval等。 - 微任務(Micro Tasks):包括
Promise的callback(.then,.catch,.finally)、MutationObserver、queueMicrotask等。
Event Loop 在每個宏任務完成後,會優先處理所有的微任務隊列,確保微任務在下一個宏任務之前全部執行完畢。這樣可以保證更高優先級的任務(微任務)先於普通任務(宏任務)執行。
完整流程
- 執行同步函式,加入 Call Stack。
- 呼叫非同步函式,相關callback被推入相應的 Queue。
- 同步函式執行完畢,Event Loop 開始檢查 Micro Task Queue。
- 執行所有 Micro Task Queue 的任務(微任務)。
- 從 Task Queue 取下一個任務,加入 Call Stack 執行。
- 重複步驟 3 - 5。
範例
1console.log('A'); 2 3setTimeout(() => { 4 console.log('B'); 5}, 0); 6 7Promise.resolve().then(() => { 8 console.log('C'); 9}); 10 11console.log('D'); 12
答案 : ADCB
解釋:
- 同步函式
console.log('A')執行。 setTimeout被放入 web api 執行,callback()=>console.log('B')進入 Task Queue。Promise.resolve()被調用,.then()callback 進入 Micro Task Queue。- 同步函式
console.log('D')執行。 - Call Stack 空閒,先處理 Micro Task 中的微任務。
console.log('C')印出。 - Call Stack 空閒,Micro Task 沒有任務, 取出宏任務執行,
console.log('B')印出。
1console.log('Start'); 2 3setTimeout(() => { 4 console.log('Timeout 1'); 5}, 0); 6 7Promise.resolve().then(() => { 8 console.log('Promise 1'); 9 10 setTimeout(() => { 11 console.log('Timeout 2'); 12 }, 0); 13 14 Promise.resolve().then(() => { 15 console.log('Promise 2'); 16 }); 17}); 18 19console.log('End'); 20
答案 : Start, End, Promise 1, Promise2, Timeout 1, Timeout 2
解釋:
- 同步函式
console.log('Start')執行。 setTimeout 1被放入 web api 執行,callback()=>console.log('Timeout 1')進入 Task Queue。Promise.resolve()被調用,.then()callback 進入 Micro Task Queue。- 同步函式
console.log('End')執行。 - Call Stack 空閒,先處理 Micro Task 中的微任務,外層的 Promise.then callback 被執行。
console.log('Promise 1')被印出。 - 在 .then callback 往下執行時遇到 setTimeout 2,其 callback被放入 Task Queue。
- 再往下執行遇到內層的Promise.resolve,其 .then() callback 被放入 Mirco Task Queue。
- Call Stack 空閒,先處理 Micro Task 中的微任務,內層的 Promise.then callback 被執行。
console.log('Promise 2')被印出。 - Call Stack 空閒,先鑒察 Micro Task ,沒有任務後,處理 Task Queue 的任務,
Timeout 1被印出。 - Call Stack 空閒,先鑒察 Micro Task ,沒有任務後,處理 Task Queue 的任務,
Timeout 2被印出。
進階題
1console.log('X'); 2 3async function foo() { 4 console.log('Y'); 5 await bar(); 6 console.log('Z'); 7} 8 9async function bar() { 10 console.log('W'); 11} 12 13foo(); 14 15Promise.resolve().then(() => { 16 console.log('V'); 17}); 18 19console.log('U'); 20
答案 : X, Y, W, U, V, Z
解釋:
- 同步函式
console.log('X')執行。 foo被呼叫,console.log('Y')被印出。bar被呼叫,console.log('W')被印出。bar是非同步函式,回傳一個resolved Promise,這使得foo中在await bar()後面的程式碼被阻塞,並放進 Micro Task Queue 作為微任務。當 JavaScript 遇到await時,它會暫停async函式的執行,等待await後面的 Promise 被解析(resolved)或被拒絕(rejected),然後再繼續執行剩下的程式。Promise.resolve().then()的 callback 進入 Micro Task Queue。console.log('U')被印出。- Call Stack 空閒,首先執行
Promise.resolve().then()的 callback ,console.log('V')被印出。 - 再來執行
foo()剩餘的程式碼,console.log('Z')被印出。
以下推薦參考資料,來自 Lydia Hallie 的影片,真的非常清楚好懂,基本上多看幾遍就能夠掌握整個Event Loop 的流程。
同場加映 Promise Execution,一樣解釋非常清楚!