返回笔记列表
01 · NOTES / WebGIS 开发

第 34 节 · 事件循环

2026 年 9 月 1 日

第 34 节 · 事件循环

📌 版本信息:基于 ECMAScript 2025 与 HTML 事件循环规范(2026-08-29 核对) 📚 来源:现代 JS 教程 · 事件循环MDN 并发模型 🎮 配套演示:loupe.dev(可视化执行顺序,强烈推荐边读边玩)

一、这一节的目标

  1. 画出事件循环的完整模型:调用栈 / Web API / 任务队列 / 微任务队列
  2. 精通宏任务与微任务的执行顺序(面试最高频题)
  3. 能人工推演一段混合代码的输出顺序
  4. 理解"长任务"为什么卡页面、怎么做让路

二、完整模型

┌────────────────────────────────────────────────┐
│  调用栈(Call Stack)                            │
│  JS 唯一执行线程:一次只跑一个函数                │
└──────────────┬─────────────────────────────────┘
               │ ①栈空了
               ▼
┌────────────────────────────────────────────────┐
│  微任务队列(Microtask)—— 优先清空              │
│  Promise.then/catch/finally、queueMicrotask     │
└────────────────────────────────────────────────┘
               │ ②微任务清空后,取一个
               ▼
┌────────────────────────────────────────────────┐
│  宏任务队列(Macrotask / Task)                  │
│  setTimeoutsetInterval、事件回调、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 之前,先学会分片。


五、自测题

  1. 微任务和宏任务各举三个来源。
  2. 一轮事件循环处理几个宏任务?微任务呢?
  3. setTimeout(fn, 0) 的真实含义是什么?能保证立刻执行吗?
  4. 为什么说"异步不能解决长任务卡顿"?
  5. 推演:Promise.resolve().then(()=>console.log(1)); setTimeout(()=>console.log(2)); queueMicrotask(()=>console.log(3)); console.log(4); 输出?

参考答案

  1. 微任务:Promise.then/catch/finally、queueMicrotask、MutationObserver;宏任务:setTimeout/setInterval、事件回调、I/O、UI 渲染相关任务。
  2. 每轮取一个宏任务;微任务整批清空(含执行中新产生的)。
  3. "最早也要等到当前同步代码 + 微任务全部结束后的下一个宏任务轮次";不能保证 0ms 立即执行(浏览器还有最小延迟约 4ms 的钳制)。
  4. 异步只是把任务排进队列,执行仍在唯一主线程;长任务占着调用栈,谁也插不进来。要真并行需 Web Worker,或分片让路。
  5. 4 1 3 2(同步 4 → 微任务 1、3 → 宏任务 2)。

六、下一步

JS 引擎机制全通 → 第 35 节:ES6+ 全特性速查,把散落在各节的现代语法一次性盘成手册。

TAGSweb