Photo by Mohammad Rahmani on Unsplash
JS 是 單執行緒
,所有同步性的工作,會一個個執行,但遇到非同步的操作就會先放到一個叫做 task queue
的地方,等到目前沒有其他工作,就會到 task queue
看看有沒有還沒執行的任務,再把它拿出來執行。
one thread
==one call stack
==one thing at a time
event loop 教學
JavaScript 執行順序
Node.js 及 JavaScript 皆是按照以下優先權順序執行程式,越前面的優先權會優先執行
1. 宏任務(MacroTask)
- script 全部代碼
- I/O
- UI Rendeting
- MessageChannel
- postMessage
2. 微任務(MicroTask)
- Process.nextTick(Node.js)
- Promise
- Object.observe(廢棄)
- MutationObserver
3. 任務佇列(task queues) 非同步事件
- click
- ajax
- setTimeout、setInterval、setImmediate
4. 繪製畫面佇列(render queue) 繪製畫面事件
若是 瀏覽器
執行 JavaScript 的話會有這個 render queue
如果畫面是 60 fps(frame per second)
的話,大約是 16.67 ms
會執行一次畫面重繪
所以如果中間有程序被 1, 2, 3
繪製畫面的程序要執行就會超過 16.67 m
才會執行
所以中間如果有程式執行太慢的話,你會覺得畫面好像卡住沒辦法動
Node.js 沒有繪製畫面的狀況,但還是有 1, 2, 3
執行順序及優先權的狀況發生
// 執行順序:1
console.log('Script Start');
// 執行順序:4
setTimeout(() => {
console.log('Set Timeout');
}, 0)
// 執行順序:3
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
// 執行順序:2
console.log('Script End');
// Script Start
// Script End
// Promise 1
// Promise 2
// Set Timeout
圖解 event loop 流程
- Stack 沒工作,執行 task queue 工作,將工作加入到 Stack 中,最早加入的工作優先處理(Queue)
- task queue 沒工作時,sleep 直到工作出現,然後執行步驟 1
前往event loop 圖解說明理解程式及事件處理流程
Call Stack 情境說明
後進先出 Last in First out,LIFO
正常 Call Stack 流程
乘法程式
// 乘法
let multiply = (a, b) => {
return a * b;
};
// 平方
let square = (n) => {
return multiply(n, n);
}
// 列印平方
let printSquare = (n) => {
let squared = square(n);
console.log(squared);
}
// 執行列印平方
printSquare(3);
Stack 狀況
Step 1. 進入程式
順序 | Stack |
---|---|
1 | main() |
Step 2. 執行 printSquare(3);
前面都是 function
定義,不會執行,直到遇到 printSquare(3);
執行程式,所以會把要執行的程式加入到 Stack
當中
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
Step 3. 執行 square(n);
進到 printSquare(3);
函式中,執行 square(n);
,將 square(n);
加入到 Stack
中
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
3 | square(n); |
Step 4. 執行 multiply(n, n);
進到 square(n);
函式中,執行 multiply(n, n);
,將 multiply(n, n);
加入到 Stack
中
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
3 | square(n); |
4 | multiply(n, n); |
Step 5. 取得 multiply(n, n); 回傳資料
在 multiply(n, n);
裡面會直接運算乘法,然後回傳資料,所以回傳後 multiply(n, n);
要執行的任務完成了
依照 Stack 後進先出 Last in First out,LIFO
的原則,會將 multiply(n, n);
移除 Stack
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
3 | square(n); |
Step 6. 取得 square(n); 回傳資料
在 square(n);
會取得 multiply(n, n);
的結果,然後直接回傳資料,所以回傳後 square(n);
要執行的任務完成了
依照 Stack 後進先出 Last in First out,LIFO
的原則,會將 square(n);
移除 Stack
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
Step 7. 列印資料 console.log(squared);
在 printSquare(3);
裡面有新的任務是要列印變數資料,所以會把 console.log(squared);
加入 Stack
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
3 | console.log(squared); |
Step 8. 列印資料完成 console.log(squared);
列印資料完成後,會將 console.log(squared);
移除 Stack
順序 | Stack |
---|---|
1 | main() |
2 | printSquare(3); |
Step 9. 完成 printSquare(3);
完成 printSquare(3);
沒有更多的任務了,會將 printSquare(3);
移除 Stack,最後遇到 main()
結束整個程式
順序 | Stack |
---|---|
1 | main() |
setTimeout Stack 狀況
console.log('Hi');
setTimeout(() => {
console.log('setTimeout Hello');
}, 3000);
console.log('End Hi');
// Hi
// End Hi
// setTimeout Hello
Step 1. 進入程式
Stack 順序 | Stack |
---|---|
1 | main() |
Step 2. 執行 console.log(‘Hi’);
遇到 console.log('Hi');
執行程式,所以會把要執行的程式加入到 Stack
當中
Stack 順序 | Stack |
---|---|
1 | main() |
2 | console.log(‘Hi’); |
Step 3. 執行完成 console.log(‘Hi’);
console.log('Hi');
執行完成後,會將 console.log('Hi');
移除 Stack
Stack 順序 | Stack |
---|---|
1 | main() |
Step 4. 執行 setTimeout();
遇到 setTimeout(callback());
執行程式,所以會把要執行的程式加入到 Stack
當中
Stack 順序 | Stack |
---|---|
1 | main() |
2 | setTimeout(callback()); |
Step 5. 執行完成 setTimeout();
setTimeout(callback());
執行時,會將裡面的 callback()
放到管理 Timer 的 webapis
然後完成 setTimeout(callback());
將 setTimeout(callback());
移除 Stack
Stack 順序 | Stack | webapis 順序 | webapis |
---|---|---|---|
1 | main() | 1 | timer(callback()) |
Step 6. 執行 console.log(‘End Hi’);
遇到 console.log('End Hi');
執行程式,所以會把要執行的程式加入到 Stack
當中
Stack 順序 | Stack | webapis 順序 | webapis |
---|---|---|---|
1 | main() | 1 | timer(callback()) |
2 | console.log(‘End Hi’); |
Step 7. 執行完成 console.log(‘End Hi’);
執行完成後,將 console.log('End Hi');
移除 Stack
Stack 順序 | Stack | webapis 順序 | webapis |
---|---|---|---|
1 | main() | 1 | timer(callback()) |
Step 8. 執行完成所有程式
執行完成所有程式後,將 main();
移除 Stack
Stack 順序 | Stack | webapis 順序 | webapis |
---|---|---|---|
1 | timer(callback()) |
Step 9. webapis 時間計時完成
當 webapis
計時完成後,會將 callback(); 放到 task queue
當中
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback(); |
Step 10. event loop 檢查 Stack 並移動 task queue
這時候 event loop
會去檢查 Stack
是否有其他任務,如果沒有其他任務,才會將 task queue
的任務以 先進先出(FIFO First In First Out)
的方式,將任務移動到 Stack
執行
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback(); |
Step 11. 執行 callback 中的 console.log(‘setTimeout Hello’);
遇到 console.log('setTimeout Hello')
執行程式,所以會把要執行的程式加入到 Stack
當中
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback(); | ||||
2 | console.log(‘setTimeout Hello’) |
Step 11. 完成執行 callback 中的 console.log(‘setTimeout Hello’);
執行完成所有程式後,將 console.log('setTimeout Hello');
移除 Stack
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback(); |
Step 12. 完成執行 callback
執行完成所有程式後,將 callback();
移除 Stack
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
多個 setTimeout Stack 狀況
setTimeout(() => {
console.log('setTimeout Hello 1');
}, 0);
setTimeout(() => {
console.log('setTimeout Hello 2');
}, 0);
setTimeout(() => {
console.log('setTimeout Hello 3');
}, 0);
// setTimeout Hello 1
// setTimeout Hello 2
// setTimeout Hello 3
Step 1. 執行所有 setTimeout();
在連續設定 3 個 setTimeout
後,會直接將 3 個程序丟給 webapis
管理
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | timer(callback1()) | ||||
2 | timer(callback2()) | ||||
3 | timer(callback3()) |
Step 2. 執行完成 setTimeout();
當 webapis
計時完成後,會將 callback(); 放到 task queue
當中
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback1() | ||||
2 | callback2() | ||||
3 | callback3() |
Step 3. 依序完成執行 callback 中的 console.log(‘setTimeout Hello’);
event loop
會檢查 Stack
是否有工作,沒有的話依序將 task queue
的工作放到 Stack 中去執行
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback() | 1 | callback2() | ||
2 | console.log(‘setTimeout Hello1’); | 2 | callback3() |
即便所有的 setTimeout
設定的時間一樣,前面的 callback1()
如果執行時間太久,會影響後面的 callback2()
及 callback3()
執行的時間
所以 setTimeout
只能保證程式執行的時間一定在指定的秒數之後,而不保證在指定的時間一定會執行,所以 setTimeout
設定 1000 ms
,則程式會在 1000 ms
後執行,可能是 1000ms
、1300ms
、1700ms
或是 1000ms + X ms
秒後執行
無窮迴圈 Stack 狀況
無窮迴圈程式
let foo = () => {
foo();
}
// RangeError: Maximum call stack size exceeded
foo();
Stack 狀況
順序 | Stack |
---|---|
1 | main() |
2 | foo() |
3 | foo() |
.. | foo() |
.. | foo() |
.. | foo() |
n | foo() |
over stack size | foo() |
執行特定事件太多次導致 task queue 阻塞
$.on('document', 'scroll', () => {
console.log('scroll!');
});
Step 1. 綁定事件到 webapis 中
執行程式時,webapi
會監聽事件,直到事件發生後,將事件需要執行的 callback()
放到 task queue
中
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | $.($.on(‘document’, ‘scroll’, callback()) |
Step 2. 觸發很多 scroll 事件
當觸發很多事件後,task queue
會有很多此次 callback() 要處理的函式
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | $.($.on(‘document’, ‘scroll’, callback()) | 1 | callback() | ||
2 | callback() | ||||
3 | callback() | ||||
4 | callback() | ||||
5 | callback() | ||||
… | callback() | ||||
n | callback() |
Step 3. 處理 callback()
然後 task queue
會持續不斷的檢查 Stack
是否有在執行程序,沒有的話再將 callback()
放到 Stack
中去執行,直到 依序
把 task queue
工作消化完成
所以在這裡就會看到,整個 task queue
被太多 callback()
佔滿的狀況,若有其他較重要的 callback
要執行,也會被原先的程序卡住
Stack 順序 | Stack | webapis 順序 | webapis | task queue 順序 | task queue |
---|---|---|---|---|---|
1 | callback() | 1 | $.($.on(‘document’, ‘scroll’, callback()) | 1 | callback() |
2 | console.log(‘scroll!'); | 2 | callback() | ||
3 | callback() | ||||
4 | callback() | ||||
5 | callback() | ||||
… | callback() | ||||
n | callback() |
所以要盡量避免使用 scroll
事件或者任何會導致 task queue
阻塞的事件,除非必要,不然會導致整個工作執行緩慢
議題狀況
執行時間太長的事件函數
當遇到需要執行比較久的工作,可以使用 setTimeout
的方式,將秒數設定為 0,然後將工作依序拆分到各個小工作,這樣可以減少 Queue 被卡住的狀況
let i = 0;
let start = Date.now();
function count() {
// do a heavy job
for (let j = 0; j < 1e9; j++) {
i++;
}
console.log("Done in " + (Date.now() - start) + 'ms');
}
count();
副作用
因為 setTimeout
的工作也是排入 Queue 去等待執行,所以若 Queue 塞滿工作時,setTimeout 設定的時間可能會有延遲的狀況發生
- 若 stack 裡頭有其他任務正在進行,setTimeout 的時間可能不會被正確觸發。
- 在 setTimeout 裡頭執行過長的任務也會導致 UI blocking。
主線任務執行時間過長
執行這一段程式碼,你會發現為什麼過了一秒,console.log
卻還沒出現
function fib(n) {
if (n < 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
setTimeout(() => console.log('hello'), 1000);
fib(40);
這是因為 fib(40)
造成的遞迴塞滿了整個 stack
,直到執行序消化完了之後,才趕緊將 task queue
的 setTimeout 拿出來。
setTimeout 任務執行時間過長
setTimeout 會先把任務放入 task queue
當中,再回到 stack
執行,若 stack 有其他 task 也會阻塞任務執行。
因此,fib(40)
執行後因為讓 stack
持續有任務,導致 500ms 過後 console.log(‘hello’) 還沒出現。
function fib(n) {
if (n < 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
setTimeout(() => {
console.log('fib(40)')
fib(40);
}, 0);
setTimeout(() => {
console.log('hello');
}, 500);
// fib(40)
// hello
Stack 與 Queue 比較
類型 | 說明 |
---|---|
Stack | 先進先出 (FIFO, First in first out) |
Queue | 後進先出 (LIFO, Last in First out) |
參考資料
- 到底 Event Loop 關我啥事?. 初學者也能懂「為什麼 JavaScript 中存在事件循環(Event Loop)?」 | by 郭耿綸 Kaleb | 無限賽局玩家 Infinite Gamer | Publication
- [面試] 前端工程師一定要會的 JS 觀念題-中英對照之上篇. 提供一個面試目錄的概念,並不會詳細解釋每一題,但會提供當初我在面試前準備的中英文… | by Hannah Lin | Starbugs Weekly 星巴哥技術專欄 | Medium
- What is an event loop in JavaScript?
- Event loop: microtasks and macrotasks
- Microtasks
- Day5 [JavaScript 基礎] Event Loop 機制 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- 一次只能做一件事情的 JavaScript,解釋 Event queue 怎能不用動畫呢 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- Worker threads | Node.js v17.8.0 Documentation
- Tasks, microtasks, queues and schedules - JakeArchibald.com
- 堆疊 Stack - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
- 佇列 Queue - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
Donate KJ 贊助作者喝咖啡
如果這篇文章對你有幫助的話,可以透過下面支付方式贊助作者喝咖啡,如果有什麼建議或想說的話可以贊助並留言給我
If this article has been helpful to you, you can support the author by treating them to a coffee through the payment options below. If you have any suggestions or comments, feel free to sponsor and leave a message for me!
方式 Method | 贊助 Donate |
PayPal | https://paypal.me/kejyun |
綠界 ECPay | https://p.ecpay.com.tw/AC218F1 |
歐付寶 OPay | https://payment.opay.tw/Broadcaster/Donate/BD2BD896029F2155041C8C8FAED3A6F8 |