Skip to content

瀏覽器的呼叫堆疊與事件循環 | 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) 的規則,就像疊盤子一樣,最後被疊上去的盤子會先被拿去執行。

當程式執行時,呼叫堆疊會按以下步驟運作:

  1. 推入(Push):函式被呼叫時,會建立一個執行環境框架(execution frame),推入堆疊頂部
  2. 執行:JavaScript 引擎執行位於堆疊最頂端的函式
  3. 彈出(Pop):函式執行完畢或遇到 return 時,從堆疊頂部移除
  4. 返回:程式控制權回到前一個函式繼續執行

範例

javascript
function firstFunc() {
  secondFunc();
}
function secondFunc() {
  thirdFunc();
}
function thirdFunc() {
  console.log('執行完畢');
}
firstFunc();

堆疊變化過程:

  1. 全域執行環境(Global Execution Context)進入堆疊
  2. firstFunc() 被呼叫,推入堆疊頂部
  3. secondFunc() 被呼叫,推入堆疊頂部
  4. thirdFunc() 被呼叫,推入堆疊頂部
  5. thirdFunc() 執行完畢,彈出堆疊
  6. secondFunc() 執行完畢,彈出堆疊
  7. firstFunc() 執行完畢,彈出堆疊
  8. 全域環境清空,程式結束

Event Loop

雖然 JavaScript 是單執行緒,但是透過執行環境(瀏覽器或 Node.js) 提供的事件循環 (Event Loop) 機制,讓耗時的非同步事件 (例如向伺服器請求資料) 不會阻礙主線程的執行,讓 JavaScript 可以在適當時機執行非同步的任務。

事件迴圈的工作流程可以分為以下步驟:

  1. 執行同步任務:所有同步程式碼在呼叫堆疊(Call Stack)中依序執行
  2. 處理異步任務:遇到 setTimeout、fetch 等異步操作時,交由 Web API 處理,完成後將回調函式放入任務佇列
  3. 檢查堆疊:當呼叫堆疊清空後,事件迴圈開始運作
  4. 優先處理微任務:先檢查微任務佇列(Microtask Queue),將所有微任務依序推入堆疊執行
  5. 處理宏任務:微任務清空後,從宏任務佇列(Macrotask Queue)取出一個任務執行
  6. 持續循環:重複上述步驟,直到所有任務完成

任務類型差異

  • 宏任務(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

答案說明

  1. 執行 macro task (執行整個 script)
  2. 第一行,放進 call stack,setTimeout 的 callback (() => console.log(1);) 放進 macro task
  3. 第二行,放進 call stack 並執行 Promise.resolve(),接著把他的 callback (() => console.log(2);) 放進 micro task
  4. 第三行,放進 call stack 並執行 Promise.resolve(),接著把他的 callback (() => setTimeout(() => console.log(3));) 放進 micro task
  5. 第四行,new Promise(() => console.log(4)); 因為 new Promise 的參數是同步的,所以 () => console.log(4) 會直接放到 call stack 裡面並被立刻執行。印出 4
  6. 第五行,放進 call stack 並執行 setTimeout,把 () => console.log(5); 放進 macro task
  7. call stack 清空,所以開始處理 micro task queue,其中最先放進去的第二行 (第 3 步) 放到 call stack 並執行。印出 2
  8. 接續執行 micro task queue 中的下一個 (第 4 步),將 setTimeout callback (() => console.log(3)) 放進 macro task,此時裡面已經有其他的 macro task 所以會排在最後面
  9. macro task queue 中剩下的都是 setTimeout 的 callback,所以依序進行以下步驟,放進 call stack 清除該 macro task 執行下一個,依序印出 153

這個 網站 視覺化 Event Loop 的執行,可以嘗試看看

參考

最後更新時間:

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog