# Day 6：JavaScript 程式碼執行排序：遞迴函數、Call Stack、Task Queue

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2025-11-27

---

> Call Stack；遞迴函數與 call stack 的關係；Task Queue（非同步的概念再釐清）

Call Stack
----------

試著理解遞迴函數運作方式的過程，感覺需要瞭解 JS 如何排序要執行的指令，聯想到 [Day1-3](https://paragraph.com/@gcake/day-1-3) 使用 Vitest readline 的時候讀到的非同步和任務排序的概念

查閱文件和科普說明，發現一樣是任務排序但是不同情境的東西：

[![](https://paragraph.com/editor/youtube/play.png)](https://www.youtube.com/watch?v=8aGhZQkoFbQ)

[![Video](https://storage.googleapis.com/papyrus_images/b98b750016e52cae5e753e99f29a68178928c541d37067c68f5f2b4d79004109.jpg)](https://www.youtube.com/watch?v=8aGhZQkoFbQ)

What the heck is the event loop anyway? | Philip Roberts | JSConf EU

JavaScript's call stack/event loop/task queue 互動關係的視覺化操作界面： [loupe](http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D)

這個視覺化工具對於理解瀏覽器如何執行被 JavaScript 程式碼定義的行為很有幫助

規劃好程式佇列和任務執行順序，才能有流暢的使用者體驗（不卡頓）

*   JavaScript 一次只能做一件事（一個程式碼片段），瀏覽器有提供 WebAPI ，透過搭配 event loop 的方式同時處理多個任務
    
*   JavaScript 會在 call stack 中依序堆疊要執行的程式碼片段，越往後被呼叫的 function 被堆疊在越上方，會被優先執行並從堆疊中清除。在堆疊中的單一程式碼片段如果執行時間太久，整個流程會全部卡住，此時瀏覽器無法執行任何動作或反應／渲染，稱為 blocking 阻塞。
    
*   task queue 會依序排列 WebAPI 處理好的任務，等候排入 call stack 執行。排列順序會按照 Macro/micro task 判斷。
    
*   event loop 的作用是去監控堆疊（call stack）和工作佇列（task queue），當堆疊已經完全清空、沒有執行項目的時候，便把佇列中的內容拉到堆疊中去執行。
    

**參考文件**

*   [\[JS\] 理解 JavaScript 中的事件循環、堆疊、佇列和併發模式（Learn event loop, stack, queue, and concurrency mode of JavaScript in depth）](https://pjchender.dev/javascript/js-event-loop-stack-queue/)
    
*   [What the heck is the event loop anyway?](https://www.youtube.com/watch?v=8aGhZQkoFbQ&list=WL&index=51&t=32s)
    
*   [Concurrency model and Event Loop - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop)
    

遞迴函數和 Call Stack
----------------

利用 function 內自我呼叫的方式堆疊 call stack 後再回收，以 MDN 官方文件舉例的程式碼來看：

    function foo(i) {
      if (i < 0) {
        return;
      }
      console.log(`begin: ${i}`);
      foo(i - 1);
      console.log(`end: ${i}`);
    }
    foo(3);
    
    

步驟

呼叫（目前執行的那一層）

call stack（頂端在上）

console.log 結果

遞迴呼叫動作

1

`foo(3)`

`[foo(3)]`<br>`[global]`

`begin: 3`

呼叫 `foo(2)`

2

`foo(2)`

`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`begin: 2`

呼叫 `foo(1)`

3

`foo(1)`

`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`begin: 1`

呼叫 `foo(0)`

4

`foo(0)`

`[foo(0)]`<br>`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`begin: 0`

呼叫 `foo(-1)`

5

`foo(-1)` 進 `if`，`return`

`[foo(-1)]`<br>`[foo(0)]`<br>`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]` → `foo(-1)` 結束後彈出

無輸出

無（直接 return）

6

回到 `foo(0)`，執行遞迴後面的程式

`[foo(0)]`<br>`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`end: 0`

無（這層遞迴已呼叫過）

7

`foo(0)` 執行完，`return`

`foo(0)` 從堆疊彈出 → 剩：<br>`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

無

無

8

回到 `foo(1)`，執行遞迴後面的程式

`[foo(1)]`<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`end: 1`

無

9

`foo(1)` 執行完，`return`

`foo(1)` 彈出 → 剩：<br>`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

無

無

10

回到 `foo(2)`，執行遞迴後面的程式

`[foo(2)]`<br>`[foo(3)]`<br>`[global]`

`end: 2`

無

11

`foo(2)` 執行完，`return`

`foo(2)` 彈出 → 剩：<br>`[foo(3)]`<br>`[global]`

無

無

12

回到 `foo(3)`，執行遞迴後面的程式

`[foo(3)]`<br>`[global]`

`end: 3`

無

13

`foo(3)` 執行完，`return`

`foo(3)` 彈出 → 剩：<br>`[global]`

無

無

最終輸出順序：

*   `begin: 3`
    
*   `begin: 2`
    
*   `begin: 1`
    
*   `begin: 0`
    
*   `end: 0`
    
*   `end: 1`
    
*   `end: 2`
    
*   `end: 3`
    

Task Queue
----------

現代說法通常會分成「task（或 macrotask）queue」和「microtask queue」，是兩個不同的佇列。

整個 JavaScript 程式碼執行順序（就是 event loop 主要在做的事）大致是：

1.  如果 call stack 不為空，就把目前的任務執行到結束（同步程式碼、或某個已經放進 stack 的 task/microtask）。
    
2.  當 call stack 清空時：
    
    *   先把 microtask queue 清到空（途中新增的 microtask 也要優先處理完）。
        
    *   然後從 task（macrotask）queue 取出最舊的一個任務，放進 call stack 執行。
        
3.  在一輪 task 執行完、microtask queue 清空後，瀏覽器才有機會做一次畫面渲染。
    

### 目前有練習到的 microtask：async/await (Promise 語法糖)

[Day1-3](https://paragraph.com/@gcake/day-1-3) 練習 Vitest 有用到 async/await 處理終端機輸入。當程式執行到 `await somePromise` 時：

    async function main() {
      const input = await question('請輸入數字：');
      // ... 後面的處理
    }
    

*   先「暫停」目前這個 async 函式，讓它後面的程式碼不要繼續往下跑。
    
*   等 somePromise 被 resolve/reject 之後（如使用者輸入數字後），恢復這個 async 函式的後半段執行；這個「恢復動作」會被排進 microtask queue。
    

其他 microtask handler 和原始的 Promise 寫法還沒練習到。

**參考文件來源**：

*   [Promise - JavaScript - MDN Web Docs](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise)
    
*   [await - JavaScript - MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)
    
*   [Using microtasks in JavaScript with queueMicrotask() - MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide)
    
*   [Event loop: microtasks and macrotasks - JavaScript.info](https://javascript.info/event-loop)
    

簡單 Recap：從遞迴函數開始理解 JavaScript 執行規則
----------------------------------

*   起點：想理解遞迴函數怎麼執行（`foo(3) → 2 → 1 → 0 → -1`，再一層層往回印出結果）。
    
*   發現：遞迴本身只用到 **call stack 的同步行為**（先進後出），跟非同步任務排序無關——整段遞迴從頭到尾都在同一個 task 裡一次跑完。
    
*   延伸：這引導到理解「JS 單執行緒 + event loop」的完整機制：
    
    *   **call stack**：存放正在執行的函式，一次只能處理一個任務。
        
    *   **task queue（macrotask）**：存放 `setTimeout`、DOM 事件等非同步任務。
        
    *   **microtask queue**：存放 Promise handler、`async/await` 後半段等微任務。
        
    *   **event loop 規則**：stack 清空後，先清空 microtask queue，再從 task queue 取下一個任務。
        
*   結論：遞迴 = call stack 堆疊與彈出；非同步 = event loop 在不同佇列間排程。兩者分別對應「單一任務內的執行流程」與「任務之間的排程順序」。
    

這樣整個程式碼執行排序規則大致上就理解了。

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-6)*
