事件循环思维导图(文字版)
┌─────────────────────────┐
│ 事 件 循 环 │
│ Event Loop(中枢) │
└───────────┬─────────────┘
┌───────────────┬───────────┼───────────────┬────────────────┐
▼ ▼ ▼ ▼ ▼
【1 为什么】 【2 组成】 【3 任务分类】 【4 执行规则】 【5 易错点】
主图(逐层展开)
事件循环 Event Loop
│
├─ 1️⃣ 为什么需要
│ ├─ JS 是单线程(操作 DOM,避免并发冲突、无锁)
│ ├─ 单线程 ⇒ 耗时任务会阻塞(渲染卡死、点击无响应)
│ └─ 解法:把耗时活儿"外包"给宿主,主线程不等待 → 异步
│
├─ 2️⃣ 五个组成部分
│ ├─ 调用栈 Call Stack ──────── 同步代码执行(LIFO)
│ ├─ 宿主环境 Web APIs ──────── 定时器 / 网络 / IO(真·别的线程)
│ ├─ 宏任务队列 Macrotask ───── setTimeout、事件、IO、script 整体
│ ├─ 微任务队列 Microtask ───── Promise.then、await 后续、queueMicrotask
│ └─ 事件循环 Event Loop ────── 栈空时搬运队列任务上栈
│
├─ 3️⃣ 任务分类(背)
│ ├─ 宏任务:script 整体 / setTimeout / setInterval / 事件 / IO / postMessage
│ ├─ 微任务:Promise.then/catch/finally / await 后的代码 / queueMicrotask
│ │ / MutationObserver / process.nextTick(Node,最高优先)
│ └─ ⚠️ new Promise(fn) 的 fn 是同步的,只有 .then 是微任务
│
├─ 4️⃣ 执行规则 ⭐
│ ├─ ① 先跑完同步代码
│ ├─ ② 栈空 → 清空【整个】微任务队列(期间新增的也要清完)
│ ├─ ③ 取【一个】宏任务执行
│ ├─ ④ 立刻回到 ②(再清微)
│ └─ 口诀:同步 → 微(全清) → 宏(取一) → 微(全清) → 宏(取一) …
│
├─ 5️⃣ 决定顺序的关键:入队时刻 ⭐⭐
│ ├─ Promise.resolve().then(cb) → 依赖的 promise 已 resolved ⇒ 当场入队
│ ├─ 链式 .then(A).then(B) 的 B → 等 A 跑完、P1 resolve ⇒ 才入队
│ ├─ await 后面的代码 → await 那一刻入队(微任务)
│ └─ 结论:书写顺序 ≠ 执行顺序,只看「谁先入队」
│
├─ 6️⃣ 易错点
│ ├─ setTimeout(fn, 0) 不是立刻 → 至少等同步 + 微任务跑完
│ ├─ try/catch 抓不到异步回调里的异常(栈已退出)
│ ├─ 微任务里无限 queueMicrotask ⇒ 微队列永不清空 ⇒ 页面彻底卡死
│ └─ 异步 ≠ 多线程:回调仍在主线程串行执行(真并行要 Web Worker)
│
└─ 7️⃣ 差异
├─ 浏览器:微任务清空后、下一宏任务前,可能插入渲染(rAF 在渲染前)
└─ Node:nextTick 队列 > Promise 微任务队列(Node 11+ 与浏览器对齐)
循环流程图(一图看懂)
┌──────────────────────┐
│ 执行同步代码(栈) │
└──────────┬───────────┘
▼
栈空了吗? ── 否 ──► 继续跑同步
│ 是
▼
┌─────────────────────────────┐
│ 清空【整个】微任务队列 │◄──┐
│ (含执行中新增的微任务) │ │
└─────────────┬───────────────┘ │
▼ │
┌──────────────────────┐ │
│ 取【一个】宏任务执行 │────────┘
└──────────────────────┘
记忆卡(贴墙版)
| 概念 |
一句话 |
| 单线程 |
同一时刻只有一段 JS 在跑 |
| 异步 |
不等待,稍后回调(≠ 多线程) |
| 宏任务 |
setTimeout / 事件 / IO —— 一次取一个 |
| 微任务 |
Promise.then / await 后续 —— 一次全清空 |
| 优先级 |
微 > 宏;nextTick > 微(Node) |
| 顺序判定 |
看入队时刻,不是书写位置 |
![事件循环-思维导图]()