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 |