前言
剛接觸 JavaScript 的時候不知道大家有沒有看過這個情境:
|
|
你覺得跑完上面的程式碼後,最後 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(呼叫堆疊)的地方等待被執行。舉一個簡單的例子來說:
|
|
這時候程式碼的運作會像是這樣:
如果程式碼都是這樣執行的話,為什麼一開始的例子,setTimeout 裡的內容會是最後才出現呢?
Task Queue(任務佇列)
這是因為非同步的程式碼排隊的地方和同步的程式碼不同。非同步的程式碼通常會有一個 callback function,而他會被放到 Task Queue(任務佇列)裡等待,當 Call Stack 沒有任務要執行時,他才會被取出來,放到 Call Stack 裡被執行。舉例來說:
|
|
執行順序會是:
那誰會負責處理 Call Stack 裡面的任務?又是誰負責把 Task Queue 裡面的任務移動到 Call Stack 內讓他們被執行的?
Event Loop(事件迴圈)
Event Loop(事件迴圈)是 JavaScript 的核心機制之一,負責監控 Call Stack 和 Task Queue,確保非同步程式碼能夠正確執行。
而 Event Loop 會幫忙做這些事情:
- 先執行 Call Stack 裡的所有同步程式碼
- 如果 Call Stack 清空了,Event Loop 會檢查 Microtask Queue(例如
Promise.then()
),如果有,則執行 - 當 Microtask Queue 清空後,Event Loop 會檢查 Task Queue,並將第一個等待的回調函式推入 Call Stack 執行
- 重複這個過程,確保所有程式碼依照適當的順序執行
我們在上面有提到 Call Stack 和 Task Queue,那 Microtask Queue 又是什麼?
Microtask Queue(微任務佇列)
除了 Task Queue 之外,還有一個優先級別更高的佇列:Microtask Queue(微任務佇列)。會在這裡排隊的任務也都是非同步程式碼,在這之中會有我們很常使用到的一個人:Promise.then()
。一樣讓我們用一個簡單的例子來看一下:
|
|
總結
讓我們來回顧一下這篇文章我們學到了什麼:
- 同步程式碼 就像在隊伍裡排隊結帳,一行一行地執行,進入 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 了嗎?😉
|
|