在前一篇文章中,我們了解到了 Callback、Promise、async / await 等實現非同步操作的方法,但有一個更關鍵的問題:
答案是因為我們有!! 事件循環(Event Loop) !JavaScript 是單執行緒,為什麼可以非同步?
單執行緒
JavaScript 本身是單執行緒,也就是說 JS 執行程式時:
- 同一時間只能執行一段同步程式碼
- 只有一個 Call stack (呼叫堆疊)
在程式運行時,能立刻執行的程式會直接進 Call stack,當 Call stack 清空後,程式才會繼續,所以如果程式中需要長時間運行的任務,就會阻塞其他程式碼執行。
Call Stack 呼叫堆疊
Call stack 是一個追蹤函式的資料結構,從 "stack" 這個名字應該也很好猜測,他是一個後進先出的結構。當函式被呼叫時會放到 stack 頂端,執行完後丟出。
非同步程式的執行步驟
JavaScript 是單執行緒,透過執行環境 (瀏覽器或 Node.js) 的API 可以實現非同步操作。
快速看個例子
console.log("start");
setTimeout(() => {
console.log("timeout");
}, 1000);
console.log("end");
這段程式碼的輸出會是:
start
end
timeout
可以看到 end 會出現在 timeout 之前 ,為什麼會是這樣呢? 為甚麼他們不會按照順序印出呢? 這就是因為有 event loop 悄悄的在運作~🪄
注意:即使 setTimeout() 設定 0 毫秒,也仍會在 end 後才印出
非同步的概念在【JavaScript 入門:實現非同步函式:callback、promise、async/await】有詳細介紹過,如果忘記或是還不熟悉的朋友可以先回去看看唷!
JavaScript 執行環境的組成
不論是在瀏覽器或是 Node.js,JavaScript 的執行環境都包含了:
- Call Stack (後進先出)
- Web APIs
- Task Queue (先進先出)
- Event Loop
要注意 API 是由瀏覽器或 Node.js 提供,而非 JS 語言內建的!
非同步程式的執行流程
一樣用剛剛的程式做範例,這邊的 setTimeout 是一個非同步的例子
setTimeout(() => {
console.log("timeout");
}, 1000);
第一步:進入 Call Stack
setTimeout() 進入 call stack
// Call Stack
setTimeout()
第二步:交給 Web API
callback 與延遲 1000ms 工作會被交給 Web API 進行,setTimeout() 立刻返回後從 Call Stack 彈出,同時計時器就在背景進行倒數
// Web APIs
timer (1000ms)
*同一時間,其他程式被擺入 call stack 執行*
第三步:時間到 → 回呼函式進入 Task Queue
當計時器倒數完 1000ms 後,會將 Callback 丟到 Task Queue
//Task Queue:
() => console.log("timeout")
第四步:Event Loop 開始工作
Event loop 會檢查 call stack 空了沒,空了就去 task queue 把任務塞到 call stack 中。
() => console.log("timeout") 被從 task queue 移到 call stack 執行
第五步:結束!
有了 Web API 與 Event loop 協調執行順序,我們的程式才不會被需要等待的任務卡住。
Event loop 運作機制
簡單來說,Event loop 的工作就是當 Call stack 空了,去 Task queue 找任務再擺到 Call stack 中,但你也知道實際上不像說起來這麼簡單!!
在 JS 中有兩種 Queue:
- Microtask Queue (微任務):promise.then()、catch()、finally()、queueMicrotask
- Macrotask Queue (宏任務):script(整體程式碼)、setTimeout、setInterval、DOM 事件
Event loop 的運作步驟
- 執行第一個宏任務(例如整個 Script)
- Call stack 被清空
- 清空所有 Microtask queue(包括執行中產生的,也就是說 Microtask 是可以插隊到 Macrotask 前)
- 取出一個 Macrotask queue
- 重複步驟 2
實戰範例
console.log("start");
setTimeout(()=>{
console.log("timeout");
}, 0);
Promise.resolve().then(()=> console.log("promise"));
console.log("end");
這段程式碼的輸出結果會是什麼呢~~~
答案
start
end
promise
timeout
步驟
- 執行第一個宏任務 (整個 script),所以會先印出 "start"
- 遇到 setTimeout,交給 web API,回呼函式丟到 Macrotask queue
- 遇到 Promise.then(),回呼函式丟到 Microtask queue
- 印出 "end",第一個宏任務( script) 結束
- Call stack 空了
- 清空 Microtask queue,印出 "promise"
- 取出第一個 Macrotask queue,印出 "timeout"
總結
JavaScript 本身是單執行緒,靠著執行環境(瀏覽器或 Node)提供的 API 與 Event loop 進行排程,實現非同步操作。Event loop 在 Call stack 清空後從 Queue (microtask 與 macrotask)中取出任務放到 Call stack 執行。
最後總結一下大家都做了些什麼:
- Callback 解決「結果何時回來」
- Promise 解決「Callback 的回呼地獄」
- async/await 解決「可讀性問題」
- Event loop 解決「非同步任務的排程問題」
參考













