瀏覽器的呼叫堆疊與事件循環 | Call Stack & Event Loop | Advance Web Development Quiz | #4
此文章是 FrontendMaster 課程的筆記
問題:排序以下數字在主控台上出現的順序
javascript
setTimeout(() => console.log(1));
Promise.resolve().then(() => console.log(2));
Promise.resolve().then(() => setTimeout(() => console.log(3)));
new Promise(() => console.log(4));
setTimeout(() => console.log(5));Call Stack 呼叫堆疊
呼叫堆疊是 JavaScript 引擎用來管理和追蹤函式執行順序的資料結構,它遵循 LIFO (Last in First out) 的規則,就像疊盤子一樣,最後被疊上去的盤子會先被拿去執行。
當程式執行時,呼叫堆疊會按以下步驟運作:
- 推入(Push):函式被呼叫時,會建立一個執行環境框架(execution frame),推入堆疊頂部
- 執行:JavaScript 引擎執行位於堆疊最頂端的函式
- 彈出(Pop):函式執行完畢或遇到 return 時,從堆疊頂部移除
- 返回:程式控制權回到前一個函式繼續執行
範例
javascript
function firstFunc() {
secondFunc();
}
function secondFunc() {
thirdFunc();
}
function thirdFunc() {
console.log('執行完畢');
}
firstFunc();堆疊變化過程:
- 全域執行環境(Global Execution Context)進入堆疊
- firstFunc() 被呼叫,推入堆疊頂部
- secondFunc() 被呼叫,推入堆疊頂部
- thirdFunc() 被呼叫,推入堆疊頂部
- thirdFunc() 執行完畢,彈出堆疊
- secondFunc() 執行完畢,彈出堆疊
- firstFunc() 執行完畢,彈出堆疊
- 全域環境清空,程式結束
Event Loop
雖然 JavaScript 是單執行緒,但是透過執行環境(瀏覽器或 Node.js) 提供的事件循環 (Event Loop) 機制,讓耗時的非同步事件 (例如向伺服器請求資料) 不會阻礙主線程的執行,讓 JavaScript 可以在適當時機執行非同步的任務。
事件迴圈的工作流程可以分為以下步驟:
- 執行同步任務:所有同步程式碼在呼叫堆疊(Call Stack)中依序執行
- 處理異步任務:遇到 setTimeout、fetch 等異步操作時,交由 Web API 處理,完成後將回調函式放入任務佇列
- 檢查堆疊:當呼叫堆疊清空後,事件迴圈開始運作
- 優先處理微任務:先檢查微任務佇列(Microtask Queue),將所有微任務依序推入堆疊執行
- 處理宏任務:微任務清空後,從宏任務佇列(Macrotask Queue)取出一個任務執行
- 持續循環:重複上述步驟,直到所有任務完成
任務類型差異
宏任務(Macrotask)
- 包含:
- script (整理程式碼)
- setTimeout / setInterval callback、postMessage、MessageChannel
- UI 渲染任務
- 用戶輸入事件
- 網路事件
- 一次只執行一個任務
- 包含:
微任務(Microtask)
- 包含:
- Promise.then() / .catch() / .finally()
- queueMicrotask()
- async/await
- MutationObserver callback
- 每次會執行到清空
- 包含:
這兩個任務都遵循 FIFO (First in First out),最早被放進去的會最先被取出來執行。
答案:
4 2 1 5 3答案說明
- 執行 macro task (執行整個 script)
- 第一行,放進 call stack,setTimeout 的 callback (
() => console.log(1);) 放進 macro task - 第二行,放進 call stack 並執行
Promise.resolve(),接著把他的 callback (() => console.log(2);) 放進 micro task - 第三行,放進 call stack 並執行
Promise.resolve(),接著把他的 callback (() => setTimeout(() => console.log(3));) 放進 micro task - 第四行,
new Promise(() => console.log(4));因為 new Promise 的參數是同步的,所以() => console.log(4)會直接放到 call stack 裡面並被立刻執行。印出 4 - 第五行,放進 call stack 並執行 setTimeout,把
() => console.log(5);放進 macro task - call stack 清空,所以開始處理 micro task queue,其中最先放進去的第二行 (第 3 步) 放到 call stack 並執行。印出 2
- 接續執行 micro task queue 中的下一個 (第 4 步),將 setTimeout callback (
() => console.log(3)) 放進 macro task,此時裡面已經有其他的 macro task 所以會排在最後面 - macro task queue 中剩下的都是 setTimeout 的 callback,所以依序進行以下步驟,放進 call stack 清除該 macro task 執行下一個,依序印出 1、5、3
這個 網站 視覺化 Event Loop 的執行,可以嘗試看看
