01 · NOTES / WebGIS 开发
第 34 节 · 事件循环
2026 年 9 月 1 日
第 34 节 · 事件循环
📌 版本信息:基于 ECMAScript 2025 与 HTML 事件循环规范(2026-08-29 核对) 📚 来源:现代 JS 教程 · 事件循环 | MDN 并发模型 🎮 配套演示:loupe.dev(可视化执行顺序,强烈推荐边读边玩)
一、这一节的目标
- 画出事件循环的完整模型:调用栈 / Web API / 任务队列 / 微任务队列
- 精通宏任务与微任务的执行顺序(面试最高频题)
- 能人工推演一段混合代码的输出顺序
- 理解"长任务"为什么卡页面、怎么做让路
二、完整模型
┌────────────────────────────────────────────────┐
│ 调用栈(Call Stack) │
│ JS 唯一执行线程:一次只跑一个函数 │
└──────────────┬─────────────────────────────────┘
│ ①栈空了
▼
┌────────────────────────────────────────────────┐
│ 微任务队列(Microtask)—— 优先清空 │
│ Promise.then/catch/finally、queueMicrotask │
└────────────────────────────────────────────────┘
│ ②微任务清空后,取一个
▼
┌────────────────────────────────────────────────┐
│ 宏任务队列(Macrotask / Task) │
│ setTimeout、setInterval、事件回调、I/O │
└────────────────────────────────────────────────┘
▲
│ Web API:定时器到点/请求完成,
│ 把回调放进对应队列等叫号
一轮循环(tick):执行完调用栈 → 清空全部微任务 → 取一个宏任务 → 再清空微任务 → 取下一个宏任务……
⚠️ 关键细节:每跑完一个宏任务就清一次微任务队列——微任务的优先级碾压宏任务,且"插队插到底"。
三、推演训练(背下这两题)
console.log('1'); // 同步 → 立即
setTimeout(() => console.log('2')); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出:1 4 3 2
// 解释:同步(1,4) → 微任务(3) → 宏任务(2)
setTimeout(() => {
console.log('A');
Promise.resolve().then(() => console.log('B')); // 在宏任务里新生的微任务
}, 0);
Promise.resolve().then(() => console.log('C'));
Promise.resolve().then(() => console.log('D'));
// 输出:C D A B
// 解释:第一轮微任务清空(C,D) → 宏任务 A → A 执行中产生的微任务 B 立即被清空(不需要再等下一个宏任务)
记忆口诀:同步先行,微任务插到底,宏任务一次一个。
四、长任务:页面卡顿的真凶
"单线程"意味着:一个函数跑 5 秒,页面就卡 5 秒(点不动、画不了、地图拖不动)。这不是异步能解决的——异步只是"排班",不增加人手:
// ❌ 长任务:一次性处理 10 万条数据,主线程被独占数秒
const result = hugeGeoJSON.features.map(complexCalc);
// ✅ 分片让路:每处理一批就 setTimeout(0) 让出主线程(渲染得以插队)
async function processInChunks(list, size = 5000) {
const out = [];
for (let i = 0; i < list.length; i += size) {
out.push(...list.slice(i, i + size).map(complexCalc));
await new Promise((r) => setTimeout(r)); // 让路:插入一个宏任务间隔
}
return out;
}
进阶词汇(认识即可,后面模块正式用):
- Web Worker:真正开"第二条线程"跑重计算(不共享 DOM),GeoJSON 大规模简化/空间计算的正确解法
- requestAnimationFrame:与渲染帧同步的回调(动画循环专用)
- requestIdleCallback:浏览器空闲时才跑(低优先级任务)
💡 地图关联:Leaflet 拖动流畅 = 它把渲染安排进 rAF;你的"10 万点过滤"卡顿 = 长任务——第 15 模块 deck.gl 之前,先学会分片。
五、自测题
- 微任务和宏任务各举三个来源。
- 一轮事件循环处理几个宏任务?微任务呢?
setTimeout(fn, 0)的真实含义是什么?能保证立刻执行吗?- 为什么说"异步不能解决长任务卡顿"?
- 推演:
Promise.resolve().then(()=>console.log(1)); setTimeout(()=>console.log(2)); queueMicrotask(()=>console.log(3)); console.log(4);输出?
参考答案
- 微任务:Promise.then/catch/finally、queueMicrotask、MutationObserver;宏任务:setTimeout/setInterval、事件回调、I/O、UI 渲染相关任务。
- 每轮取一个宏任务;微任务整批清空(含执行中新产生的)。
- "最早也要等到当前同步代码 + 微任务全部结束后的下一个宏任务轮次";不能保证 0ms 立即执行(浏览器还有最小延迟约 4ms 的钳制)。
- 异步只是把任务排进队列,执行仍在唯一主线程;长任务占着调用栈,谁也插不进来。要真并行需 Web Worker,或分片让路。
- 4 1 3 2(同步 4 → 微任务 1、3 → 宏任务 2)。
六、下一步
JS 引擎机制全通 → 第 35 节:ES6+ 全特性速查,把散落在各节的现代语法一次性盘成手册。
TAGSweb