编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

    • 综合案例

    • 专栏博客

      • README
      • 引擎解析编译执行
      • 隐藏类与回收机制
      • 类型隐式转换精算
      • 作用域链闭包原理
      • 函数绑定规则组合
      • 原型链语法糖本质
      • 代理与元编程协议
      • 事件循环承诺机制
        • 1. 案例与疑问引入
          • 1.1 一个点击后 3
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构全景概览
          • 2.1 浏览器事件循环完
          • 2.2 为什么微任务必须
        • 3. 宏任务队列详解
          • 3.1 script
          • 3.2 不同宏任务源的优
          • 3.3 浏览器 vs N
        • 4. 微任务队列详解
          • 4.1 Promise.
          • 4.2 Mutation
          • 4.3 queueMic
        • 5. Promise实
          • 5.1 构造函数 + 三
          • 5.2 then 的链式
          • 5.3 递归解析 the
        • 6. Promise实
          • 6.1 catch
          • 6.2 all / ra
          • 6.3 A+ 测试套件跑
        • 7. 六大反模式详解
          • 7.1 构造器反模式
          • 7.2 嵌套地狱 vs
        • 8. async实现
          • 8.1 Generator
          • 8.2 await 的微
          • 8.3 for…of
        • 9. 错误并发控制
          • 9.1 try/catch
          • 9.2 五个并发方法的精
          • 9.3 三层错误防护策略
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 实现一个支持重试
          • 10.3 设计哲学回扣
          • 10.4 速查表
      • 工作线程并发调度
      • 页面渲染像素原理
      • 网络接口存储架构
      • 服务端运行时编程
      • 模块系统双轨操作
      • 现代工程链三件套
      • 设计模式函数哲学
      • 跨端架构终局总结
  • CodeX
  • JavaScript入门
  • 专栏博客
杨充
2026-06-11
目录

事件循环承诺机制

# 08.事件循环 Promise 一体论

📍 上接第 07 篇《Proxy 元编程与迭代协议》。同步世界的工具已就位。本文进入 JS 的灵魂——异步。事件循环为什么先 micro 后 macro?Promise 的状态机怎么保证"状态不可逆"?async/await 到底脱糖成了什么?手写一个跑通 872 条测试用例的 Promise,它需要哪几块核心代码?

# 目录介绍

  • 1. 案例与疑问引入
    • 1.1 一个点击后 3
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构全景概览
    • 2.1 浏览器事件循环完
    • 2.2 为什么微任务必须
  • 3. 宏任务队列详解
    • 3.1 script
    • 3.2 不同宏任务源的优
    • 3.3 浏览器 vs N
  • 4. 微任务队列详解
    • 4.1 Promise.
    • 4.2 Mutation
    • 4.3 queueMic
  • 5. Promise实
    • 5.1 构造函数 + 三
    • 5.2 then 的链式
    • 5.3 递归解析 the
  • 6. Promise实
    • 6.1 catch
    • 6.2 all / ra
    • 6.3 A+ 测试套件跑
  • 7. 六大反模式详解
    • 7.1 构造器反模式
    • 7.2 嵌套地狱 vs
  • 8. async实现
    • 8.1 Generator
    • 8.2 await 的微
    • 8.3 for…of
  • 9. 错误并发控制
    • 9.1 try/catch
    • 9.2 五个并发方法的精
    • 9.3 三层错误防护策略
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 实现一个支持重试
    • 10.3 设计哲学回扣
    • 10.4 速查表

# 1. 案例与疑问引入

# 1.1 一个点击后 3

先看一段在生产环境真实跑过的代码——一个表单编辑页面,每输入一个字符都会触发"自动保存"逻辑。用户反馈:"打完一行字,点了保存按钮,3 秒之后才弹提示":

// auto-save.js —— 自动保存引擎(故障版本)
let isDirty = false;
const pendingSaves = [];

// 监听输入——每输入一个字符就标记"脏了"
input.addEventListener('input', () => {
  isDirty = true;
});

// 每 500ms 检查一次是否需要保存
setInterval(() => {
  if (isDirty) {
    isDirty = false;
    pendingSaves.push(Date.now());

    // "自动保存"——把当前内容序列化并写入 localStorage
    Promise.resolve().then(() => {
      const data = editor.getValue();
      localStorage.setItem('draft', data);  // ← 同步 API——阻塞微任务!
      console.log('Auto-saved at', pendingSaves.shift());

      // "保存完成后"再触发一次检查(以防下次输入太快)
      Promise.resolve().then(() => {
        if (isDirty) {
          // 又脏了?再排一次队
          isDirty = false;
          pendingSaves.push(Date.now());
          Promise.resolve().then(() => {
            localStorage.setItem('draft', editor.getValue());
            pendingSaves.shift();
          });
        }
      });
    });
  }
}, 500);

// 保存按钮——用户点它期望立刻响应
saveBtn.addEventListener('click', () => {
  console.log('Save clicked!');
  // ...发送网络请求保存数据...
  alert('Saved!');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

现象:

  • 快速输入 20 个字符(每 200ms 一个字符)→ 每 500ms 触发一次 Promise.resolve().then 保存 → 微任务队列有 5 个在排队的 localStorage.setItem
  • 用户在输入过程中点击"保存"按钮 → click 事件(宏任务)被放入宏任务队列
  • 但当前宏任务(setInterval 回调)的微任务队列还没清空——有 5 个 localStorage.setItem('draft', ...) 在排队,每个耗时 ~3ms
  • click 事件要等当前宏任务执行完 + 所有微任务清空之后才能执行
  • 总延迟 = 5 × 3ms(localStorage 写入)= 15ms——理论上不严重
  • 但用户输入了 200 个字符(持续打字 40 秒)→ pendingSaves 累积了几十个 → 微任务队列里积压了几十个 Promise.resolve().then 的嵌套 → 点击按钮到弹 alert 延迟超过 3 秒

核心问题不是 localStorage.setItem 慢——而是微任务在清空队列时是"一口气"全部执行的,不会中断去处理下一个宏任务。

# 1.2 顺藤摸到根因

带着这条线往下挖:

  • 假设 1:是不是 localStorage.setItem 太慢了?——单独测试:一次 setItem 约 1-3ms。20 次确实只要 60ms。但这里是"嵌套的微任务",每次的 then 回调里又会触发新的 then——形成一个"微任务瀑布"。浏览器会一口气执行完所有这些嵌套微任务——不给宏任务(如 click 事件)任何插队机会。
  • 假设 2:那为什么不用 setTimeout 代替 Promise.resolve().then?——因为 setTimeout 是宏任务——它排在当前宏任务之后的队列中。如果改成 setTimeout,保存操作会在当前宏任务结束、微任务清空、渲染之后再执行——不会阻塞 click 事件的响应。但代价是:保存延迟从 0ms(微任务立即执行)变成 ~4ms(宏任务到下一轮才执行)——对自动保存来说可接受。
  • 假设 3:那为什么浏览器不设置"微任务执行上限"?——因为微任务的设计初衷就是"在当前宏任务结束前,尽快完成所有轻量更新"。如果限制微任务数量,就会有些 Promise.then 的回调被推迟到下一轮——违背了"then 的回调在当前宏任务结束前执行"的规范承诺。ECMA-262 规范没有规定微任务的数量上限——实现者(V8/JSC/SpiderMonkey)可以自行决定。
  • 假设 4:V8 有防护吗?——有。V8 内部有"微任务递归深度检查"——如果一个微任务中产生的微任务数量超过一定阈值(V8 的 kMaxRecursion),会触发警告。但这不是硬限制——理论上你可以让微任务嵌套到内存耗尽。V8 的设计哲学是:不对微任务执行次数设硬上限,但如果明显在制造"无限微任务"则报警。

# 1.3 我们要回答什么

这段代码里至少藏着 7 个原理点:

① 事件循环一个 tick 的完整流程:宏任务→微任务→渲染——每一步谁在等谁?   → 第 2 章
② 微任务为什么是"一口气清空"而不是一次执行一个?                       → 第 2.2 节
③ 宏任务有哪六种来源?它们的优先级为什么不同?                          → 第 3 章
④ MutationObserver 为什么选择微任务而非宏任务或同步回调?               → 第 4.2 节
⑤ Promise/A+ 规范要求 then 的回调必须是异步的——这个"异步"怎么保证?      → 第 5.2 节
⑥ async/await 脱糖后到底是什么?Babel 为什么把它变成 Generator+Promise? → 第 8 章
⑦ Promise.allSettled 和 Promise.any 的短路语义为什么不一样?            → 第 9.2 节
1
2
3
4
5
6
7

本篇路线:

架构总图(第 2 章)
   ↓
宏任务队列(第 3 章)──→ 解开"六种宏任务的优先级和调度差异"
   ↓
微任务队列(第 4 章)──→ 解开"微任务的执行时机和 V8 内部检查点"
   ↓
手写 Promise(第 5~6 章)──→ 解开"Promise/A+ 872 条用例的每一条要求"
   ↓
六大反模式(第 7 章)──→ 解开"生产代码中的高频陷阱"
   ↓
async/await 本质(第 8 章)──→ 解开"语法糖是怎么变成 Generator 的"
   ↓
错误处理和并发(第 9 章)──→ 解开"五种并发方法的短路与不短路语义"
   ↓
综合案例(第 10 章)──→ 案例彻底剖开 + 哲学四条 + 速查表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

📌 本篇定位:这是 JS 异步的总枢纽篇——第 03~07 篇讲的是"同步世界的语法和工具",本篇讲"异步世界的根本机制"。事件循环是 JS 单线程并发模型的基石;Promise 是异步操作的统一抽象。读完本篇后,你对每一行 await、每一个 .then()、每一个 setTimeout 在什么时机执行,不再有模糊地带。

# 2. 架构全景概览

# 2.1 浏览器事件循环完

┌──────────────────────────────────────────────────────────────────┐
│              浏览器事件循环 —— 一个 tick 的完整流程                   │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ ① 从宏任务队列中取出一个 task(FIFO)                        │   │
│  │   ┌────────────────────────────────────────────────────┐  │   │
│  │   │ 执行这个 task —— 一段同步的 JS 代码                   │  │   │
│  │   │ 这可能是:script 整体执行 / setTimeout 回调 / 事件回调  │  │   │
│  │   └────────────────────────────────────────────────────┘  │   │
│  └──────────────────────┬───────────────────────────────────┘   │
│                         │ 当前 task 执行完毕                     │
│                         ▼                                       │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ ② 清空微任务队列(Microtask Checkpoint)                    │   │
│  │   ┌────────────────────────────────────────────────────┐  │   │
│  │   │ while (microtaskQueue.length > 0) {                 │  │   │
│  │   │   const microtask = microtaskQueue.shift();         │  │   │
│  │   │   microtask.run();                                  │  │   │
│  │   │   // ⚠️ 如果这个微任务产生了新的微任务 → 追加到队列尾部  │  │   │
│  │   │   // → 同一轮全部清空!                               │  │   │
│  │   │ }                                                    │  │   │
│  │   └────────────────────────────────────────────────────┘  │   │
│  └──────────────────────┬───────────────────────────────────┘   │
│                         │                                       │
│                         ▼                                       │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ ③ 检查是否需要渲染(如果有渲染管线任务)                      │   │
│  │   ┌────────────────────────────────────────────────────┐  │   │
│  │   │ if (需要渲染) {                                      │  │   │
│  │   │   requestAnimationFrame 回调执行                     │  │   │
│  │   │   → Style Recalc → Layout → Paint → Composite       │  │   │
│  │   │ }                                                    │  │   │
│  │   └────────────────────────────────────────────────────┘  │   │
│  └──────────────────────┬───────────────────────────────────┘   │
│                         │                                       │
│                         ▼                                       │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ ④ 回到步骤 ① —— 取下一个宏任务                              │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

V8 内部——Microtask Checkpoint 的实现位置:

在 V8 源码中,微任务的"清空点"不是由 JS 代码触发的——它嵌入在 V8 执行完每一段 JS 代码之后。具体在 src/execution/microtask-queue.cc 中的 MicrotaskQueue::RunMicrotasks() 函数。每当 V8 的 Invoke() 函数执行完一段 JS(如一个 script 标签、一个事件回调、一个 setTimeout 回调),V8 都会检查微任务队列——如果有待执行的微任务,就在这里同步清空它们,然后才把控制权交还给事件循环。

# 2.2 为什么微任务必须

疑惑:如果浏览器"每次只执行一个微任务,然后检查是否需要渲染",是不是就能解决第 1 章的按钮延迟问题?

论证:

这种方案的问题在于:微任务的"轻量"假设被打破了。

微任务被设计成"在两次可能引发渲染的时机之间,给开发者一个尽快完成轻量更新的机会"。规范假设的是:Promise.then 里的回调都很轻——更新一个 DOM 属性、改一个变量——几微秒完成。在这种假设下,"一口气全清空"是合理的——与其每次微任务后检查一次渲染(开销大),不如全部跑完再渲染(一次渲染覆盖所有变更)。

但如果微任务里的回调做了重任务(如 localStorage.setItem 同步写入磁盘 3ms),"一口气全清空"就会变成"一口气卡死主线程"。

正确的思路不是"改变微任务的清空策略"——而是"不要把重任务放进微任务":

任务类型 放哪里 原因
更新一个 DOM 属性(el.style.color = 'red') 微任务(Promise.then) 轻量——1µs 完成,且需要在当前帧渲染前生效
读一个 DOM 布局属性(el.offsetHeight) 不需要微任务——直接读 已经在同步代码中
写入 localStorage(磁盘 I/O) 宏任务(setTimeout) 重——3ms,不应该阻塞其他微任务和渲染
发送网络请求(fetch) 本来就是异步的——直接调用 fetch 本身就是宏任务(I/O 回调)
大量 DOM 插入(1000 个元素) requestAnimationFrame 对齐渲染帧——在帧开始时做

结论:微任务的"一口气清空"策略是正确的——前提是开发者遵守轻量承诺。如果这个承诺被打破(在微任务里做重 CPU/磁盘 I/O 操作),修复方向是"把重操作移到宏任务",而不是"修改浏览器的清空策略"。

# 3. 宏任务队列详解

# 3.1 script

疑惑:同样是宏任务,为什么"点击按钮"和"setTimeout 回调"在 Chrome 里的调度行为不同?

论证——浏览器内部不是"一个队列",而是多个队列,每种事件来源(task source)有自己的队列:

┌───────────────────────────────────────────────────────────────┐
│           浏览器内部的多个宏任务队列(task sources)               │
├───────────────────────────────────────────────────────────────┤
│                                                               │
│  ① DOM 操作任务源:                                              │
│     来源:<script> 标签、document.write                          │
│     优先级:最高——这是"当前正在跑的代码"                            │
│                                                               │
│  ② 用户交互任务源:                                              │
│     来源:click / input / keydown / scroll 等                  │
│     优先级:高——用户输入应该被尽快响应(~1ms 内)                    │
│                                                               │
│  ③ 网络任务源:                                                  │
│     来源:fetch 回调 / XHR 回调 / WebSocket 消息                 │
│     优先级:中——网络 RTT 远大于调度延迟,提高调度优先级无意义          │
│                                                               │
│  ④ 定时器任务源:                                                │
│     来源:setTimeout / setInterval                             │
│     优先级:中低——浏览器会等更高优先级队列清空后才处理它们               │
│     ⚠️ 这个队列受 4ms 夹持约束(第 09 篇展开)                     │
│                                                               │
│  ⑤ 媒体任务源:                                                  │
│     来源:<video> / <audio> 事件                                │
│     优先级:高——音视频播放不能卡                                   │
│                                                               │
│  ⑥ 消息任务源:                                                  │
│     来源:postMessage / MessageChannel                         │
│     优先级:中高——用于跨窗口/Worker 通信,不希望被延迟              │
│                                                               │
│  事件循环的调度逻辑(HTML Spec §8.1.4.2):                        │
│  ┌──────────────────────────────────────────────────────┐      │
│  │ 每次"选下一个宏任务"时:                                  │      │
│  │   1. 优先从"用户交互"队列取(对用户点击的响应必须快)        │      │
│  │   2. 然后从"消息"队列取                                   │      │
│  │   3. 然后从"网络"和"定时器"队列取                          │      │
│  │   4. "定时器"可能被推迟到多个 tick 之后                     │      │
│  │                                                         │      │
│  │ 这不是"严格的优先级队列"——而是"计算机选择"(Picking a Task) │      │
│  │ 浏览器可以根据自己的策略决定从哪个队列取                     │      │
│  └──────────────────────────────────────────────────────┘      │
│                                                               │
└───────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

六种宏任务来源对比:

来源 典型 API 触发时机 相对优先级 嵌套夹持
DOM 操作 <script> 执行 页面加载时 最高 —
用户交互 click / input / scroll 真实用户操作时 高 —
消息通信 postMessage 代码调用 postMessage 时 中高 —
网络 fetch 回调 / XHR.onload 网络响应就绪时 中 —
定时器 setTimeout / setInterval 延迟到期时 中低 嵌套≥5 层 → 4ms
媒体 <video>.onplay 媒体事件触发时 高 —

# 3.2 不同宏任务源的优

// 测试:postMessage 和 setTimeout 谁先执行?
setTimeout(() => console.log('setTimeout'), 0);

const channel = new MessageChannel();
channel.port1.onmessage = () => console.log('postMessage');
channel.port2.postMessage(null);
// postMessage 内部调用:channel.port2.postMessage → 触发 port1.onmessage

// Chrome 输出:postMessage → setTimeout
// 原因:MessageChannel 属于"消息任务源"——优先级高于"定时器任务源"
1
2
3
4
5
6
7
8
9
10

React Scheduler 就利用了这个差异:React 的并发模式需要"高优先级但又是宏任务"的调度原语(既要能中断、又要优先级高于 setTimeout)。React 选择 MessageChannel 来实现 scheduler——因为 postMessage 是宏任务(可被更高级的事件中断),但优先级高于 setTimeout(不会被 4ms 夹持)。

# 3.3 浏览器 vs N

维度 浏览器 Node.js
事件循环模型 HTML Spec §8.1.4 — macroTask→microTask→render libuv 六阶段 — timers→pending→poll→check→close
宏任务类 6 种来源队列(DOM/用户交互/网络/定时器/媒体/消息) 4 种阶段(timers/poll/check/close)
微任务 Promise.then + MutationObserver + queueMicrotask Promise.then + process.nextTick + queueMicrotask
setImmediate ❌ ✅ check 阶段执行
渲染管线 ✅ ❌
nextTick ❌ ✅ 优先级高于 Promise.then

详细对比见第 12 篇《Node.js 运行时与流式编程》第 3 章。

# 4. 微任务队列详解

# 4.1 Promise.

疑惑:Promise.then(fn) 的回调 fn 到底什么时候被加入微任务队列?是 .then() 被调用时,还是 Promise resolve 时?

论证——这取决于 Promise 当前的状态:

// 场景 A:Promise 已经 settled——then 立即入队微任务
const p1 = Promise.resolve('done');   // p1 已经是 fulfilled
p1.then(v => console.log('A:', v));   // ← 立刻注册微任务
console.log('sync');
// → sync → A: done

// 场景 B:Promise 还是 pending——then 的回调先存储,等 resolve 时再入队
let resolveP2;
const p2 = new Promise(r => { resolveP2 = r; });
p2.then(v => console.log('B:', v));   // ← 回调被存储到 p2 的 [[PromiseFulfillReactions]] 列表中
console.log('sync');
// → sync('B' 还没输出——因为 p2 还没 resolve)
resolveP2('done');                    // ← 此时把回调加入微任务队列
// → B: done
1
2
3
4
5
6
7
8
9
10
11
12
13
14

V8 内部——PromiseReaction 的执行路径:

Promise.resolve(value)
   │
   ▼
① p.[[PromiseState]] = fulfilled, p.[[PromiseResult]] = value
   │
   ▼
② 遍历 p.[[PromiseFulfillReactions]](存储所有 then 的回调)
   │
   ▼
③ 对每个 reaction:
   → 创建一个 PromiseReaction Job(微任务)
   → EnqueueJob("PromiseJobs", PromiseReactionJob)
   → 把 Job 加入 V8 的微任务队列
   │
   ▼
④ 当前宏任务执行完毕 → V8 的 Microtask Checkpoint 到达
   │
   ▼
⑤ 执行 PromiseReactionJob:
   → 调用 reaction.handler(value)
   → 拿到返回值 → resolve 下一个 Promise(链式调用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 4.2 Mutation

疑惑:如果我在同一个同步代码块中改变 DOM 10 次,MutationObserver 的回调会触发几次?

论证——一次,且以微任务形式触发:

let count = 0;
const observer = new MutationObserver(() => {
  console.log('Mutation called, count =', ++count);
});
observer.observe(document.body, { childList: true });

// 同步代码块:连续修改 DOM 10 次
for (let i = 0; i < 10; i++) {
  document.body.appendChild(document.createElement('div'));
}
console.log('sync done');
// → sync done
// → Mutation called, count = 1 ← 只触发了一次!

// 为什么是一次?
// 每次 appendChild 时,浏览器把 mutation record 追加到 observer 的内部缓冲区
// 当前宏任务执行完毕 → V8 进入 Microtask Checkpoint → 清空微任务队列
// → observer 的回调被放在微任务队列中 → 此时浏览器才"通知"你——
//   把 10 条 mutation records 打包成一个数组传给你
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

为什么选微任务而不是同步回调:

如果 MutationObserver 是同步的(每次 appendChild 立刻触发回调),那 10 次 DOM 修改 = 10 次同步回调。如果回调里又修改了 DOM(响应式更新)→ 触发更多 MutationObserver → 递归 → "无法停止的连锁反应"。微任务把多个同步变更聚合为一次异步通知——既保证了"所有变更都被记录",又避免了同步反馈循环。

# 4.3 queueMic

// queueMicrotask:标准微任务入口(ECMA-262 §27.5)
queueMicrotask(() => console.log('via queueMicrotask'));

// Promise.resolve().then:同样入微任务——但多一次 Promise 创建开销
Promise.resolve().then(() => console.log('via Promise'));

// 两者在同一个微任务队列中,按注册顺序执行
queueMicrotask(() => console.log('1'));
Promise.resolve().then(() => console.log('2'));
// → 1 → 2

// 性能差异:queueMicrotask 不创建 Promise 对象 → 更轻量
// 推荐:意图是"往微任务队列放一个回调" → 用 queueMicrotask
//      意图是"在 Promise 链上追加操作" → 用 .then()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 5. Promise实

# 5.1 构造函数 + 三

疑惑:Promise 的状态为什么必须是"不可逆"的?resolve 之后不能变 reject,反之亦然?

论证——规范要求 ECAM-262 §27.2.1.2:

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  constructor(executor) {
    this.state = PENDING;         // ← 初始:等待中
    this.value = undefined;       // fulfilled 时的值
    this.reason = undefined;      // rejected 时的原因
    this.onFulfilledCallbacks = [];  // 等状态变更后要执行的成功回调
    this.onRejectedCallbacks = [];   // 等状态变更后要执行的失败回调

    const resolve = (value) => {
      // ⚠️ 状态只能从 PENDING 变成 FULFILLED——不能逆转!
      if (this.state !== PENDING) return;
      this.state = FULFILLED;
      this.value = value;
      // 状态变了 → 异步执行所有等待的 onFulfilled 回调
      this.onFulfilledCallbacks.forEach(fn => fn());
    };

    const reject = (reason) => {
      // ⚠️ 同样:只能从 PENDING 变成 REJECTED
      if (this.state !== PENDING) return;
      this.state = REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn());
    };

    try { executor(resolve, reject); }
    catch (e) { reject(e); }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

为什么必须不可逆:

// 如果状态可逆——反例:
// resolve() 之后又 reject() → then 的第一个回调执行了 → 但状态又被改成 rejected
// → 同一个 Promise 的 then 到底该调用哪个回调? → 不可预测

// ❌ 如果状态可逆——这段代码的行为无法确定:
const p = new Promise((resolve, reject) => {
  resolve('A');   // 状态 → fulfilled
  reject('B');    // 如果允许这行执行 → 状态倒回到 rejected → 破坏确定性
});
p.then(v => console.log('then:', v), e => console.log('catch:', e));
// then 和 catch 哪个会执行?取决于 resolve 和 reject 被调用的顺序——
// "不确定性"是异步编程的最大敌人
1
2
3
4
5
6
7
8
9
10
11
12

结论:不可逆的状态机是 Promise 的基石——它保证了"一个 Promise 只会有一个结果",消除了异步操作中"结果来回变"的混乱。

# 5.2 then 的链式

then(onFulfilled, onRejected) {
  // 值穿透:如果没传 onFulfilled → 用默认函数把值传给下一个 then
  onFulfilled = typeof onFulfilled === 'function'
    ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function'
    ? onRejected : reason => { throw reason; };

  // then 必须返回一个新 Promise(链式调用的关键)
  const promise2 = new MyPromise((resolve, reject) => {
    const handleFulfilled = () => {
      // ⚠️ A+ 规范 §2.2.4:onFulfilled 必须异步调用(不能同步!)
      queueMicrotask(() => {
        try {
          const x = onFulfilled(this.value);
          // 递归解析:x 可能是普通值(直接 resolve)或另一个 Promise(等它 settle)
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    };

    const handleRejected = () => {
      // ⚠️ A+ 规范 §2.2.4:onRejected 同样必须异步调用
      queueMicrotask(() => {
        try {
          const x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    };

    // 根据当前状态决定:立刻执行 或 存储等待
    if (this.state === FULFILLED) {
      handleFulfilled();    // ← 已 fulfilled → 立刻把回调入微任务
    } else if (this.state === REJECTED) {
      handleRejected();
    } else { // PENDING
      // ← 还是 pending → 把回调存起来,等状态变更时再调用
      this.onFulfilledCallbacks.push(handleFulfilled);
      this.onRejectedCallbacks.push(handleRejected);
    }
  });
  return promise2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

为什么 then 的回调必须是异步的(A+ §2.2.4):

// 如果回调是同步的——反例:
const p = Promise.resolve('A');
p.then(v => console.log('1:', v));
console.log('2: sync');
// 如果 then 同步执行 → 1: A → 2: sync (先输出 1)
// 如果 then 异步执行 → 2: sync → 1: A (先输出 2)

// A+ 规范选择了后者:回调永远是异步的。
// 原因:保证"行为一致"。
// 即使 Promise 已经 settled,then 的回调也不应该"突然在同步代码中间执行"。
// 这让开发者可以把 Promise.then 写在同步代码的任何位置——
// 而不担心"这个 then 会不会突然打断我的同步代码"。
1
2
3
4
5
6
7
8
9
10
11
12

# 5.3 递归解析 the

这是 Promise/A+ 规范中最复杂的部分。resolvePromise 要处理四种情况:x 是 promise2 自身(循环引用)、x 是 MyPromise 实例、x 是 thenable 对象、x 是普通值。

function resolvePromise(promise2, x, resolve, reject) {
  // ① 循环引用——'promise2' 和 'x' 指向同一个对象
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // ② x 是 MyPromise 实例——直接接管它的状态
  if (x instanceof MyPromise) {
    x.then(resolve, reject);
  }
  // ③ x 是 thenable 对象(有 .then 方法的对象/函数)
  else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    let called = false;  // ← 防止 resolve 和 reject 都被调用
    try {
      const then = x.then;  // 只取一次——防止 getter 副作用
      if (typeof then === 'function') {
        then.call(x,
          y => {
            if (called) return; called = true;
            // 递归解析——y 可能又是一个 Promise
            resolvePromise(promise2, y, resolve, reject);
          },
          r => {
            if (called) return; called = true;
            reject(r);
          }
        );
      } else {
        resolve(x);  // x.then 不是函数 → 当作普通值
      }
    } catch (e) {
      if (called) return; called = true;
      reject(e);
    }
  }
  // ④ 普通值——直接 resolve
  else {
    resolve(x);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

called 标志位的作用:

// 场景:一个 thenable 同时调用了 resolve 和 reject
const maliciousThenable = {
  then(resolve, reject) {
    resolve('A');   // ← 第一次调用 → 应该生效
    reject('B');    // ← 第二次调用 → 应该被忽略!
  }
};

// 如果没有 called 标志位:
// resolve('A') → promise2 变成 fulfilled (value='A')
// reject('B')  → promise2 变成 rejected (reason='B') ← 把 A 覆盖了!
// → 状态不可逆被打破!

// 有了 called 标志位:
// resolve('A') → called = true → promise2 fulfilled
// reject('B')  → called 已经 true → return(什么都不做)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 6. Promise实

# 6.1 catch

// catch = 只传 onRejected 的 then
catch(onRejected) {
  return this.then(null, onRejected);
}

// finally = "无论如何都要执行这个回调——但不要改变决议"
finally(onFinally) {
  return this.then(
    // fulfilled 路径:先执行 finally 回调,然后把原值继续往下传
    value => MyPromise.resolve(onFinally()).then(() => value),
    // rejected 路径:先执行 finally 回调,然后把原因继续往下抛
    reason => MyPromise.resolve(onFinally()).then(() => { throw reason; })
  );
  // ⚠️ 关键点:
  //   1. finally 的回调不接收参数(不关心值是啥——只关心"时候到了")
  //   2. finally 新返回 Promise 继续前一个 Promise 的 value/reason
  //   3. 但如果 finally 的回调返回了 rejected Promise → 决议被"覆盖"!
}

// 验证 finally 的"穿透"行为:
const p = Promise.resolve('hello');
p.finally(() => console.log('cleanup'))
 .then(v => console.log(v));        // → cleanup → hello ← 值穿透了!

// 但如果 finally 回调返回 rejected Promise:
p.finally(() => Promise.reject('override'))
 .then(v => console.log(v))         // ← 不会执行!
 .catch(e => console.log(e));      // → override ← 被 finally 的 rejection 覆盖了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 6.2 all / ra

// resolve / reject:快速创建 settled Promise
static resolve(value) {
  if (value instanceof MyPromise) return value;  // 已经是 Promise → 直接返回
  return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
  return new MyPromise((_, reject) => reject(reason));
}

// all:全部成功 → 成功;任一个失败 → 失败(短路)
static all(promises) {
  return new MyPromise((resolve, reject) => {
    const result = [];
    let count = 0;
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        value => { result[i] = value; if (++count === promises.length) resolve(result); },
        reject  // ← 任一个失败直接 reject(短路:后面的不再等待)
      );
    }
  });
}

// race:最快的决定结果(无论成功或失败)
static race(promises) {
  return new MyPromise((resolve, reject) => {
    for (const p of promises) {
      MyPromise.resolve(p).then(resolve, reject);
      // ← 第一个 settle 的 Promise 直接决定结果(后面的"比赛"被忽略)
    }
  });
}

// allSettled:全部跑完,不管成功失败——每个的结果都记录下来
static allSettled(promises) {
  return new MyPromise(resolve => {
    const result = [];
    let count = 0;
    const handle = (i, status, val) => {
      result[i] = { status, [status === 'fulfilled' ? 'value' : 'reason']: val };
      if (++count === promises.length) resolve(result);
    };
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        v => handle(i, 'fulfilled', v),
        r => handle(i, 'rejected', r)
      );
    }
  });
}

// any:第一个成功 → 成功(短路);全失败 → AggregateError
static any(promises) {
  return new MyPromise((resolve, reject) => {
    let count = 0;
    const errors = [];
    for (let i = 0; i < promises.length; i++) {
      MyPromise.resolve(promises[i]).then(
        resolve,  // ← 第一个成功立刻 resolve(短路)
        err => {
          errors[i] = err;
          if (++count === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        }
      );
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

五种并发方法的精确语义:

方法 何时决议 短路? ES 版本
Promise.all 全成功 → resolve;任一失败 → reject ✅(失败) ES2015
Promise.race 第一个 settle 的 Promise 决定结果 ✅(第一个) ES2015
Promise.allSettled 全跑完 → resolve(永不 reject) ❌ ES2020
Promise.any 任一成功 → resolve;全失败 → AggregateError ✅(成功) ES2021

# 6.3 A+ 测试套件跑

Promise/A+ 测试套件(promises-aplus-tests)包含 872 条测试用例,核心验证点:

测试类别 覆盖内容 典型边界条件
2.1 状态 三个状态和状态转换 resolve() 后 reject() → 应被忽略;executor 抛异常 → 应 reject
2.2 then then 的链式调用和异步性 then 回调不传 → 值穿透;回调抛异常 → 下一个 then 的 onRejected 应收到
2.3 Promise 解析 thenable 的递归解析 循环引用检测;thenable 里的 getter 抛异常;resolve() 里传 Promise 本身
3.x 静态方法 resolve/reject/all/race 等 all 传空数组;race 传空数组;传 already-settled Promise

适配器代码:

module.exports = {
  resolved: MyPromise.resolve.bind(MyPromise),
  rejected: MyPromise.reject.bind(MyPromise),
  deferred: () => {
    let resolve, reject;
    const promise = new MyPromise((res, rej) => { resolve = res; reject = rej; });
    return { promise, resolve, reject };
  }
};
// 运行:npx promises-aplus-tests ./my-promise.js
1
2
3
4
5
6
7
8
9
10

# 7. 六大反模式详解

# 7.1 构造器反模式

// ❌ 反模式 1:构造器反模式——把已有的 Promise 包在 new Promise 里
function fetchData() {
  return new Promise((resolve, reject) => {
    fetch('/api').then(resolve).catch(reject);
    // fetch 本身返回 Promise——你不需要再包一层!
  });
}
// ✅ 直接返回
function fetchData() { return fetch('/api'); }

// ❌ 反模式 2:忘记 return——链断掉了
fetch('/user')
  .then(res => { res.json(); })   // ← 没 return!下一个 then 拿到 undefined
  .then(user => console.log(user)); // → undefined
// ✅ 记得 return
fetch('/user').then(res => res.json()).then(user => console.log(user));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 7.2 嵌套地狱 vs

// ❌ 反模式 3:嵌套地狱——.then 里面又 .then(不是链式!)
fetch('/a').then(a => {
  fetch('/b').then(b => { /* b 依赖于 a */ });
});
// ✅ 扁平链——每个 then 返回一个 Promise
fetch('/a').then(a => fetch('/b')).then(b => { /* b */ });

// ❌ 反模式 4:串行执行独立请求——浪费 3×时间
await fetch('/a'); await fetch('/b'); await fetch('/c');  // 三次串行,各 200ms
// ✅ 并行——1×时间
const [a, b, c] = await Promise.all([fetch('/a'), fetch('/b'), fetch('/c')]);

// ❌ 反模式 5:不处理 rejection——静默吞掉错误
fetch('/api');  // 如果网络失败 → UnhandledPromiseRejectionWarning → Node 14+ 会杀进程
// ✅ 总是加 .catch() 或 try/catch

// ❌ 反模式 6:forEach + async——并行但不可控(详见 §8.3)
urls.forEach(async url => { await fetch(url); });
// ✅ 用 for...of + await(顺序可控)
for (const url of urls) { await fetch(url); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 8. async实现

# 8.1 Generator

疑惑:async function 在被 Babel 转换后,变成了 Generator + Promise——但这两种机制为什么刚好能组合?

论证——async/await 本质上是利用 Generator 的"暂停-恢复"能力来装饰 Promise 链:

// ① 源码(ES2017)
async function fetchUser(id) {
  const user = await fetch(`/user/${id}`);
  const orders = await fetch(`/orders/${user.id}`);
  return { user, orders };
}

// ② Babel 脱糖后的等价代码(简化后):
function fetchUser(id) {
  return _asyncToGenerator(function*() {
    const user = yield fetch(`/user/${id}`);     // ← await 变成 yield
    const orders = yield fetch(`/orders/${user.id}`);
    return { user, orders };
  })();
}

// ③ _asyncToGenerator 的核心实现:
function _asyncToGenerator(genFn) {
  return function() {
    return new Promise((resolve, reject) => {
      const gen = genFn.apply(this, arguments);

      function step(key, arg) {
        let result;
        try {
          result = gen[key](arg);  // gen.next(arg) 或 gen.throw(e)
        } catch (e) {
          return reject(e);
        }

        const { value, done } = result;
        if (done) {
          return resolve(value);   // ← Generator 执行完毕 → resolve
        }

        // ⚠️ 核心:value 是 yield 产出的 Promise → 等它 settle 再推进 Generator
        Promise.resolve(value).then(
          v => step('next', v),    // ← Promise 成功 → Generator 继续
          e => step('throw', e)    // ← Promise 失败 → Generator 抛异常
        );
      }

      step('next');  // ← 启动 Generator
    });
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

为什么 Generator + Promise 恰好够用:

Generator 的能力 async/await 如何利用它
yield 暂停函数执行 await = yield → 暂停 async 函数
gen.next(value) 恢复执行 + 传入值 await 后的 Promise resolve → .then(v => gen.next(v)) → 把值传回
gen.throw(e) 在 yield 位置注入异常 await 后的 Promise reject → .catch(e => gen.throw(e)) → 让 try/catch 捕获
return 结束 Generator Generator done = true → resolve(value)

# 8.2 await 的微

// 验证:await null 后的代码被放入微任务
async function demo() {
  console.log('A');
  await null;          // ← await 后面的所有代码 → 被包装为微任务
  console.log('B');    // ← 微任务
}
demo();
Promise.resolve().then(() => console.log('C'));
console.log('D');
// → A D B C
// 不是 A B C D(因为 await 后的代码是异步的!)
// B 和 C 都是微任务——B 先注册,所以 B 先执行
1
2
3
4
5
6
7
8
9
10
11
12

await 的等价转换:

// await expression 的等价代码(ECMA-262 §14.7.5.13):
// await expression
//   → Promise.resolve(expression).then(result => { /* await 后面的代码 */ })

async function demo() {
  console.log('A');
  await null;
  console.log('B');
}

// 等价于:
function demo() {
  console.log('A');
  return Promise.resolve(null).then(() => {
    console.log('B');  // ← 这段代码作为 then 的回调 → 被放入微任务队列
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 8.3 for…of

// ❌ forEach + async:每次迭代"发射"一个 async 函数——不等它完成
const urls = ['/a', '/b', '/c'];
urls.forEach(async url => {
  const res = await fetch(url);  // ← 每个 await 独立——三个 fetch 几乎同时发出
  console.log(url, res.status);
});
// 输出顺序不确定:a/b/c 随机——取决于哪个先返回
// 而且外层代码不会等这三个 forEach 完成——forEach 本身不返回 Promise!

// ✅ for…of + await:每次迭代等待当前 await 完成再进入下一次
for (const url of urls) {
  const res = await fetch(url);
  console.log(url, res.status);
}
// 输出顺序确定:a → b → c(串行——每次等上一个完成)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 9. 错误并发控制

# 9.1 try/catch

// ✅ async 函数内——try/catch 可以捕获 await 的 rejection
async function demo() {
  try {
    await Promise.reject('async error');
  } catch (e) {
    console.log('Caught:', e);  // → Caught: async error
  }
}
// 原理:await 把 rejection 转化为异常 → 抛给包含它的 try/catch

// ❌ 非 async 上下文——try/catch 不能捕获异步 reject
try {
  Promise.reject('sync error');
} catch (e) {
  console.log('Never runs');  // ← 永远不会执行!
}
// 原因:try/catch 只捕获同步异常。
// Promise.reject 是同步创建了一个 rejected Promise——但 rejection 本身
// 是异步传播的(通过微任务调用 onRejected)。try/catch 早就执行完了。
// → UnhandledPromiseRejectionWarning
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 9.2 五个并发方法的精

方法 成功条件 失败条件 短路? 结果顺序 ES
Promise.all 全部成功 任一失败(立即 reject) ✅ 失败短路 按输入顺序 ES2015
Promise.race 第一个成功的 第一个失败的 ✅ 短路 不保持顺序 ES2015
Promise.allSettled 永不 reject——一直等到全部完成 不会 reject ❌ 不短路 按输入顺序 ES2020
Promise.any 任一成功(立即 resolve) 全部失败 → AggregateError ✅ 成功短路 第一个成功的 ES2021
// 场景选择:
// ① 多个独立请求(一个失败全失败 = 页面核心数据) → Promise.all
// ② 多源数据(有些失败没关系,只记录结果)          → Promise.allSettled
// ③ CDN 回源(从多个 CDN 请求同一个文件,谁先到用谁)→ Promise.any
// ④ 超时检测(请求 vs 5s 定时器,谁先 settle)      → Promise.race
1
2
3
4
5

# 9.3 三层错误防护策略

┌──────────────────────────────────────────────────┐
│         Promise 错误处理的三层纵深防御               │
├──────────────────────────────────────────────────┤
│                                                  │
│  第一层:每个 Promise 链末尾加 .catch()             │
│  ┌────────────────────────────────────────────┐  │
│  │ fetch('/api')                               │  │
│  │   .then(handleSuccess)                      │  │
│  │   .catch(err => {                           │  │
│  │     showToast('操作失败');                    │  │
│  │     reportError(err);  // ← 知道具体哪个操作失败了│  │
│  │   });                                       │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
│  第二层:async 函数内用 try/catch 包裹 await        │
│  ┌────────────────────────────────────────────┐  │
│  │ async function loadPage() {                 │  │
│  │   try {                                     │  │
│  │     const data = await fetchData();         │  │
│  │     render(data);                           │  │
│  │   } catch (e) {                             │  │
│  │     showFallbackUI();                       │  │
│  │   }                                         │  │
│  │ }                                           │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
│  第三层:全局兜底(最后的防线)                        │
│  ┌────────────────────────────────────────────┐  │
│  │ window.addEventListener(                    │  │
│  │   'unhandledrejection',                     │  │
│  │   event => {                                │  │
│  │     // event.reason = 未被处理的 rejection 原因│  │
│  │     // event.promise = 被 reject 的 Promise  │  │
│  │     reportException(event.reason);          │  │
│  │     event.preventDefault();  // ← 抑制控制台警告│  │
│  │   }                                         │  │
│  │ );                                          │  │
│  └────────────────────────────────────────────┘  │
│                                                  │
└──────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的自动保存按钮延迟——七个疑问现在能逐条作答:

疑问 答案
① 事件循环一个 tick 的完整流程 第 2.1 节:取宏任务→执行→清空所有微任务→可选渲染→下一个宏任务。微任务在宏任务之间"一口气"全部清空
② 微任务为什么是一口气清空 第 2.2 节:微任务的"轻量更新"假设——规范假设 then 回调几µs 完成。如果遵守轻量承诺→一口气清空是最优的;如果违反→卡主线程。修复是"移重到宏任务"
③ 六种宏任务来源的优先级差异 第 3 章:用户交互 > 消息通信 > 网络/定时器。不同 task source 有不同队列,浏览器"选择"而非"严格优先级"
④ MutationObserver 为什么用微任务 第 4.2 节:防止同步回调连锁反应——微任务把多个同步 DOM 变更聚合为一次异步批量通知
⑤ Promise.then 的异步保证 第 5.2 节:A+ §2.2.4——then 回调必须用 queueMicrotask 或等价机制异步执行,保证行为一致性
⑥ async/await 脱糖本质 第 8 章:Generator 的暂停-恢复 + Promise 的链式推进。await = yield;.then(v => gen.next(v)) = 恢复并传入值
⑦ 五个并发方法的语义差异 第 9.2 节:all(失败短路)/ race(第一个短路)/ allSettled(不短路)/ any(成功短路)

修复方案(按代价从小到大):

方案 A:把 localStorage.setItem(同步磁盘 I/O)从微任务移到宏任务

// ❌ 原版:重操作在微任务中——阻塞所有后续宏任务
Promise.resolve().then(() => {
  localStorage.setItem('draft', data);  // ← 3ms 同步写入
});

// ✅ 修复版:重操作在宏任务中——不阻塞 click 事件等用户交互
setTimeout(() => {
  localStorage.setItem('draft', data);
}, 0);
1
2
3
4
5
6
7
8
9

方案 B:用 requestIdleCallback 做自动保存(理想方案)

// 自动保存是"低优先级任务"——用户不关心它什么时候完成
function scheduleAutoSave(data) {
  requestIdleCallback(deadline => {
    localStorage.setItem('draft', data);
  }, { timeout: 5000 });  // ← 最晚 5 秒内必须保存
}
1
2
3
4
5
6

# 10.2 实现一个支持重试

class RequestManager {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
  }

  async request(fn, { retries = 3, timeout = 10000 } = {}) {
    // 并发控制:如果正在跑的请求达到上限,排队等待
    while (this.running >= this.maxConcurrent) {
      await new Promise(r => this.queue.push(r));
    }
    this.running++;

    try {
      let lastError;
      for (let i = 0; i <= retries; i++) {
        try {
          const result = await this._withTimeout(fn(), timeout);
          return result;  // 成功 → 直接返回
        } catch (e) {
          lastError = e;
          if (i < retries) {
            // 指数退避 + jitter:200ms → 400ms → 800ms
            const base = 200 * Math.pow(2, i);
            const jitter = base * (0.5 + Math.random() * 0.5);
            await new Promise(r => setTimeout(r, jitter));
          }
        }
      }
      throw lastError;
    } finally {
      this.running--;
      if (this.queue.length > 0) this.queue.shift()();  // 排队中的下一个出队
    }
  }

  _withTimeout(promise, ms) {
    return Promise.race([
      promise,
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
      )
    ]);
  }
}

const manager = new RequestManager(3);  // 最多 3 个并发
const urls = ['/a', '/b', '/c', '/d', '/e'];
const results = await Promise.all(
  urls.map(url => manager.request(() => fetch(url), { retries: 2, timeout: 5000 }))
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 10.3 设计哲学回扣

哲学一·「单线程的并发幻觉——事件循环是对"简化 vs 能力"的工程妥协」

JS 是单线程的——但用户不可能等一个请求完成再点下一个按钮。事件循环用非抢占式调度(一个宏任务跑完才换下一个)给了用户并发的幻觉,同时避免了多线程的竞态锁和死锁噩梦。代价是:任何长任务都会阻塞后续所有任务——这就是为什么 Worker(第 09 篇)必须存在。

事件循环不是"因为 JS 引擎做不到多线程所以凑合的"——它是主动选择的设计决策:"开发者心智负担低"优先于"运行时并行度"。在 2009 年(Node.js 诞生)和 1995 年(JavaScript 诞生),这个决策是对的——当时服务器只有 1~2 核。今天 16 核是标配——所以 Worker 应运而生,在不破坏"主线程无锁心智模型"的前提下,给了并行计算的通道。

哲学二·「微任务是宏任务之间的'冲刺区'——设计为轻量,但也可以成为瓶颈」

微任务的设计意图是:在两次"可能引发渲染"的时机之间,给开发者一个尽快完成轻量更新的机会。Promise.then 注册的回调不需要等到下一帧——它就在当前宏任务结束、渲染之前执行。MutationObserver 用它来做"批量 DOM 变更的聚合通知"。微任务的"一口气清空"策略是设计选择——不是 bug。它的脆弱之处在于:假设了开发者会把轻任务放进微任务。 一旦这个假设被打破(把 localStorage.setItem 放进 then 回调)——微任务从"冲刺区"变成了"拥堵区"。

哲学三·「Promise 的不可逆状态——用'单一结果'消除不确定性,是异步编程的根本保障」

Promise 的三个状态(pending/fulfilled/rejected)只能从 pending 出发、一去不回。这个不可逆性是 Promise 比回调强大的根基——它保证了 then 的回调只会被调用一次,且只有一个结果。回调模式下,你的 callback(err, result) 可以被调用零次(忘了调)、一次、甚至两次(两次 resolve)——你完全失控。Promise 用状态机把"结果"和"通知"绑定——状态变了才通知,状态不变不通知,状态不可逆所以通知只有一次。

哲学四·「async/await 不是新事物——是 Generator + Promise 的语法封装,JS 用同一个底层能力驱动了两种控制流」

await 就是 yield——等待的"值"是 Promise,引擎用 .then() 把代码推进下一步。这不是魔术——这是 Generator(第 07 篇 §9)的暂停/恢复机制在 Promise 链上的自然延伸。JS 没有发明两套不同的异步机制——它发明了一个"暂停-恢复"的底层能力(Generator),然后把它在"同步迭代器"和"异步控制流"上各用了一次。 Generator 是语法层面的"保存当前执行位置",Promise 是值层面的"未来的结果"——两个正交的概念组合在一起,恰好能实现 async/await。

# 10.4 速查表

事件循环一个 tick 的四阶段:

阶段 发生了什么 耗时
1. 取宏任务 从宏任务队列出队并执行这一整段同步 JS 取决于任务复杂度
2. 清空微任务 一口气执行所有微任务(包括新产生的) 取决于微任务数量和每个微任务的耗时
3. 渲染(可选) rAF → Style Recalc → Layout → Paint → Composite ~5-16ms(取决于变更量)
4. 下一个宏任务 回到步骤 1 —

Promise 六大反模式速查:

反模式 症状 修复
构造器反模式 new Promise 包裹已有 Promise 直接返回已有的 Promise
忘记 return 下一个 then 拿到 undefined 每个 then 回调加 return
嵌套地狱 .then 里嵌套 .then 扁平链:每个 then 返回下一个 Promise
忽略并发 await 一个接一个(串行) Promise.all(并行)
忽略 rejection UnhandledPromiseRejectionWarning .catch() 或 try/catch
forEach + async 顺序错乱或未等待 for…of + await 或 Promise.all

五种并发方法速查:

方法 成功条件 失败条件 短路 ES
all 全部成功 任一失败 ✅ 失败 ES2015
race 第一个成功的 第一个失败的 ✅ 第一个 ES2015
allSettled 永不 reject 不会 reject ❌ ES2020
any 任一成功 全部失败 → AggregateError ✅ 成功 ES2021

async function 脱糖对照:

async/await 语法 等价 Generator + Promise
async function fn() {} function fn() { return _asyncToGenerator(function*(){}); }
await promise yield promise
await 后的代码 Promise.resolve(value).then(v => gen.next(v)) 的回调
return value resolve(value)(Generator 结束)
throw error reject(error) 或 gen.throw(error)

下一步:事件循环统一了主线程的调度。但 JS 不止一个线程——Worker 和并发即将展开。进入 09.Worker 并发与调度时钟。

上次更新: 2026/06/16, 12:36:20
代理与元编程协议
工作线程并发调度

← 代理与元编程协议 工作线程并发调度→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式