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 |