JavaScript 工程師的修行之路(1)

Event Loop、Call Stack 與非同步機制

前言

剛接觸 JavaScript 的時候不知道大家有沒有看過這個情境:

1
2
3
4
5
6
7
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

你覺得跑完上面的程式碼後,最後 log 印出來的順序是什麼呢?

如果你的答案是 A → B → C 的話,歡迎你來到我的文章!希望這篇文章可以幫助你更理解 JavaScript 的程式執行順序,理解為什麼這段程式碼跑完後,順序會是 A → C → B。

在了解執行順序之前,我們需要先認識 JavaScript 是如何處理程式碼的。

同步與非同步的概念

在 JavaScript 中,程式碼的執行方式可以分為同步(Synchronous)和非同步(Asynchronous)兩種:

  • 同步程式碼(Synchronous):程式碼會按照撰寫的順序逐行執行,當一個函式正在執行時,其他程式碼必須等它執行完畢後才能繼續。
  • 非同步程式碼(Asynchronous):某些操作可能會花費較長的時間(例如 I/O 操作、網路請求、計時器等),這些操作不會阻塞主執行緒,而是會在背景執行,等到完成後才通知 JavaScript 執行對應的回調函式。

想像你去一家早餐店點餐

同步就是你排隊點了一份三明治,然後站在櫃檯前等老闆做好才離開。這時候老闆只能處理你的訂單,無法同時幫其他客人做餐點。如果餐點需要 5 分鐘,所有其他客人都得等你先拿到才輪到他們。

而非同步則是你先點了三明治,老闆告訴你「等一下,我做好會叫你」,然後繼續幫其他客人點餐。當你的三明治做好時,老闆才喊你的號碼,你再過去拿餐。這個情境下,其他客人不需要等你拿到餐點,大家的服務變得更有效率。

在 JavaScript 中:

  • 同步程式碼 就像「站在櫃檯等老闆做好三明治」,所有程式碼會按照順序執行,直到當前的執行完成,才能繼續下一步。
  • 非同步程式碼 則像「點完餐後先去做別的事,等三明治做好了再來拿」,這樣 JavaScript 可以處理更多請求,而不會因為等待某個結果而卡住。

Call Stack(呼叫堆疊)

JavaScript 採用單線程(single-threaded)模型,代表它一次只能執行一個任務。如果有多個任務要執行時,這些任務們就必須要排隊,等待 JavaScript 執行到他們。

那這些任務會在哪裡排隊呢?通常來說,同步程式碼都會被放到一個叫 Call Stack(呼叫堆疊)的地方等待被執行。舉一個簡單的例子來說:

1
2
console.log("A");
console.log("B");

這時候程式碼的運作會像是這樣:

如果程式碼都是這樣執行的話,為什麼一開始的例子,setTimeout 裡的內容會是最後才出現呢?

Task Queue(任務佇列)

這是因為非同步的程式碼排隊的地方和同步的程式碼不同。非同步的程式碼通常會有一個 callback function,而他會被放到 Task Queue(任務佇列)裡等待,當 Call Stack 沒有任務要執行時,他才會被取出來,放到 Call Stack 裡被執行。舉例來說:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

setTimeout(() => {
  console.log("C");
}, 0);

console.log("D");

執行順序會是:

那誰會負責處理 Call Stack 裡面的任務?又是誰負責把 Task Queue 裡面的任務移動到 Call Stack 內讓他們被執行的?

Event Loop(事件迴圈)

Event Loop(事件迴圈)是 JavaScript 的核心機制之一,負責監控 Call Stack 和 Task Queue,確保非同步程式碼能夠正確執行。

而 Event Loop 會幫忙做這些事情:

  1. 先執行 Call Stack 裡的所有同步程式碼
  2. 如果 Call Stack 清空了,Event Loop 會檢查 Microtask Queue(例如 Promise.then()),如果有,則執行
  3. 當 Microtask Queue 清空後,Event Loop 會檢查 Task Queue,並將第一個等待的回調函式推入 Call Stack 執行
  4. 重複這個過程,確保所有程式碼依照適當的順序執行

我們在上面有提到 Call Stack 和 Task Queue,那 Microtask Queue 又是什麼?

Microtask Queue(微任務佇列)

除了 Task Queue 之外,還有一個優先級別更高的佇列:Microtask Queue(微任務佇列)。會在這裡排隊的任務也都是非同步程式碼,在這之中會有我們很常使用到的一個人:Promise.then()。一樣讓我們用一個簡單的例子來看一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");

總結

讓我們來回顧一下這篇文章我們學到了什麼:

  • 同步程式碼 就像在隊伍裡排隊結帳,一行一行地執行,進入 Call Stack,跑完後才換下一個
  • 非同步程式碼(像 setTimeout)會被交給 Web API 處理,等它們準備好,才會被放回來,排隊等著執行
  • Microtask Queue(像 Promise.then())的優先級比 Task Queue 高,會先執行,所以 Promise.then() 會比 setTimeout(..., 0) 先跑
  • Event Loop 就像 JavaScript 的調度員,負責監控 Call Stack 和 Task Queue,確保大家按順序執行,不會亂掉

那麼現在,你可以理解為什麼這段程式碼被印出來的順序是 A → C → B 了嗎?😉

1
2
3
4
5
6
7
console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");
Built with Hugo
Theme Stack designed by Jimmy