事件循环
建立浏览器事件循环的任务队列模型,解释同步代码、微任务与宏任务的交错顺序及工程含义。
#type / concept
#status / growing
#tech / dev
#resource / javascript
[!info] 关联笔记
事件循环
这个概念为什么出现
JS 在浏览器主线程里跑 UI 与脚本。若同步死循环,页面冻结;若异步回调乱序理解,会出现:
- 先
setTimeout后Promise.then却预测反了 - 在 effect/渲染后读到的状态时序错乱
- 把 sleep 当锁
事件循环解释:单线程如何并发地处理 I/O 与渲染相关任务。
[!abstract] 一句话理解 调用栈清空后,先尽可能清空微任务,再取下一个宏任务;渲染机会插在任务之间(浏览器实现细节有弹性)。
最小可运行示例
场景:下单按钮日志顺序排障
// 业务:点击下单后埋点与请求回调交错
console.log('1 同步:校验购物车')
setTimeout(() => {
console.log('4 宏任务:timeout 回调')
}, 0)
Promise.resolve().then(() => {
console.log('3 微任务:promise then')
})
console.log('2 同步:发起请求前')
建议运行:
node order-loop.js
# 或浏览器控制台
期望输出(确定):
1 同步:校验购物车
2 同步:发起请求前
3 微任务:promise then
4 宏任务:timeout 回调
结合场景关注点
- 同步永远先跑完当前栈。
then微任务插队在 timeout 前。0ms不是“立即”,是“尽快作为定时器任务”。
核心模型
flowchart TD
A[执行同步脚本/一个任务] --> B[清空微任务队列]
B --> C[可能渲染/更新]
C --> D[取下一个宏任务]
D --> A
宏任务 vs 微任务(浏览器教学)
| 类型 | 例子 |
|---|---|
| 宏任务 | setTimeout setInterval 部分 I/O/事件回调 |
| 微任务 | Promise.then queueMicrotask MutationObserver |
规范/宿主:HTML 的 event loop 与 ECMAScript jobs 相互衔接;Node 另有相位模型,见 Node 事件循环阶段。不要把 Node 相位硬套浏览器。
设计动机
用队列把“等待中的工作”从调用栈拆走,保持单线程可预测,同时让 I/O 完成通知稍后继续。
边界
requestAnimationFrame与渲染节奏相关,不简单等同 timeout。- 微任务过多会推迟渲染,造成卡顿。
- 竞态应用
AbortController/序号,而不是靠“延时更长”。
常见误区
[!warning] 常见误区:async/await 创造多线程 await 把后续变成 Promise 微任务链,仍在同一线程事件循环上。
工程实践
- 长计算切片或 Web Worker。
- 不要用
setTimeout当同步锁。 - 预测输出题先画“同步 → 微任务 → 宏任务”。
本节总结
事件循环是浏览器 JS 并发的时间表。会排序,才会写稳定异步与可解释的 UI 更新。
自测题
- 为什么大量同步
then链会卡渲染? await后的代码大概何时执行?
参考答案
- 微任务在渲染前被优先排空,过长会推迟绘制。
- 当前 await 的 Promise 兑现后,作为微任务续跑(在对应任务点)。
延伸阅读
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| HTML event loops | 规范 | 浏览器循环 |
| MDN Event loop | 文档 | 模型 |