Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

JavaScript 与 TypeScript / JavaScript & TypeScript

1. JavaScript 与 TypeScript 学习路线 / JavaScript and TypeScript Learning Path

2. JavaScript 核心语义:值、对象、函数与模块 / JavaScript Core Semantics: Values, Objects, Functions, and Modules

3. TypeScript 类型系统:从结构类型到类型编程 / The TypeScript Type System from Structural Typing to Type-Level Programming

4. JavaScript 异步编程与事件循环 / Asynchronous JavaScript and the Event Loop

本页目录

JavaScript 异步编程与事件循环 / Asynchronous JavaScript and the Event Loop ​

📅 创建时间:2026-07-28 🏷️ 标签:#EventLoop #Promise #AsyncAwait #Microtask #Concurrency 📚 前置知识:[[01-javascript-runtime-and-language]]


📋 本章目标 ​

  • 理解事件循环的完整运行周期:调用栈、任务队列、微任务队列、渲染管线
  • 掌握微任务与宏任务的精确执行顺序及其可视化模型
  • 深入理解 Promise 的状态机、链式调用与六大静态方法
  • 理解 async/await 的底层机制与常见误区
  • 掌握 AbortController 实现可取消的异步操作
  • 掌握异步迭代器、生成器与 Streams API 的使用场景
  • 能够识别并修复常见的异步陷阱(floating promise、竞态条件等)

第1部分:事件循环深度解析 ​

1.1 单线程的 JavaScript 为何不阻塞? ​

JavaScript 在单个执行线程上运行,但通过**事件循环(Event Loop)**协调 I/O、计时器和用户交互。关键在于:耗时操作由宿主环境(浏览器 / libuv)处理,完成后再将回调推入队列,由事件循环调度执行。

┌─────────────────────────────────────────────────────────────┐
│                    JavaScript 运行时全景                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐    ┌──────────────────────────────┐      │
│  │  Call Stack   │    │      Web APIs / libuv       │      │
│  │  (调用栈)     │    │  setTimeout, fetch,         │      │
│  │              │    │  fs.readFile, setImmediate   │      │
│  │  ┌────────┐  │    │                              │      │
│  │  │  fn3()  │  │    │  注册回调 → 宿主处理 →      │      │
│  │  │  fn2()  │  │    │  完成后回调入队              │      │
│  │  │  fn1()  │  │    └──────────────────────────────┘      │
│  │  └────────┘  │              ↓                            │
│  └──────────────┘    ┌──────────────────────────────┐      │
│         │            │       Task Queues            │      │
│         ↓            │  ┌────────────────────────┐  │      │
│  ┌──────────────┐    │  │  Microtask Queue       │  │      │
│  │  Event Loop  │←───│  │  Promise.then,         │  │      │
│  │  (事件循环)   │    │  │  queueMicrotask,       │  │      │
│  │              │    │  │  MutationObserver       │  │      │
│  │  "有任务吗?" │    │  └────────────────────────┘  │      │
│  │  "栈空了吗?" │    │  ┌────────────────────────┐  │      │
│  │  "渲染吗?"   │    │  │  Macrotask Queue       │  │      │
│  └──────────────┘    │  │  setTimeout, setInterval,│  │      │
│                      │  │  I/O, UI events          │  │      │
│                      │  └────────────────────────┘  │      │
│                      └──────────────────────────────┘      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

1.2 事件循环的一次完整 Tick ​

┌─────────────────────────────────────────────────────────────┐
│               浏览器事件循环:一次完整 Tick                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 从 Macrotask Queue 取出一个任务执行                      │
│     │                                                       │
│     ▼                                                       │
│  2. 该任务执行完毕(Call Stack 变空)                         │
│     │                                                       │
│     ▼                                                       │
│  3. 清空 Microtask Queue(包括执行过程中新产生的微任务)      │
│     │  循环执行直到微任务队列彻底为空                         │
│     │                                                       │
│     ▼                                                       │
│  4. 判断是否需要渲染(requestAnimationFrame 回调在此执行)    │
│     │  通常 60fps → 约 16.6ms 一次                           │
│     │                                                       │
│     ▼                                                       │
│  5. 回到步骤 1,取下一个 Macrotask                            │
│                                                             │
│  关键规则:                                                  │
│  • 一个 macrotask → 清空全部 microtask → 可能渲染 → 下一个  │
│  • 微任务可以插队,在同一次 tick 内全部执行完毕               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

1.3 Node.js 事件循环:libuv 六阶段 ​

Node.js 的事件循环基于 libuv,分为六个阶段,每个阶段维护自己的 FIFO 回调队列。

┌─────────────────────────────────────────────────────────────┐
│              Node.js 事件循环:libuv 六阶段                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│    ┌──────────────────────────────────────────────┐        │
│    │                 timers                        │        │
│    │  setTimeout / setInterval 到期回调             │        │
│    └─────────────┬────────────────────────────────┘        │
│                  ▼                                          │
│    ┌──────────────────────────────────────────────┐        │
│    │           pending callbacks                   │        │
│    │  系统操作(如 TCP 错误)推迟到此处执行           │        │
│    └─────────────┬────────────────────────────────┘        │
│                  ▼                                          │
│    ┌──────────────────────────────────────────────┐        │
│    │           idle, prepare                       │        │
│    │  libuv 内部使用,一般无需关心                   │        │
│    └─────────────┬────────────────────────────────┘        │
│                  ▼                                          │
│    ┌──────────────────────────────────────────────┐        │
│    │               poll                            │        │
│    │  获取新的 I/O 事件;阻塞等待(若无过期 timer)  │        │
│    │  fs.readFile、网络请求等回调在此入队            │        │
│    └─────────────┬────────────────────────────────┘        │
│                  ▼                                          │
│    ┌──────────────────────────────────────────────┐        │
│    │               check                           │        │
│    │  setImmediate 回调在此执行                     │        │
│    └─────────────┬────────────────────────────────┘        │
│                  ▼                                          │
│    ┌──────────────────────────────────────────────┐        │
│    │           close callbacks                     │        │
│    │  socket.on('close', ...) 等清理回调            │        │
│    └─────────────┬────────────────────────────────┘        │
│                  │                                          │
│                  ▼ 下一轮循环(或退出)                      │
│                                                             │
│  两个特殊的"插队"队列(每个阶段切换时清空):                 │
│  ┌──────────────────────────────────────────────┐        │
│  │  • nextTick Queue: process.nextTick()        │        │
│  │  • Microtask Queue: Promise.then,            │        │
│  │    queueMicrotask                             │        │
│  │  优先级:nextTick > Microtask                 │        │
│  └──────────────────────────────────────────────┘        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

第2部分:微任务与宏任务 ​

2.1 两类队列的本质区别 ​

┌─────────────────────────────────────────────────────────────┐
│                    Microtask vs Macrotask                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Macrotask(宏任务 / Task)                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  来源:setTimeout, setInterval, I/O, UI 事件,       │   │
│  │        setImmediate(Node), MessageChannel           │   │
│  │  调度:每个事件循环 tick 只取一个执行                 │   │
│  │  特点:两次 macrotask 之间可能发生渲染               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Microtask(微任务 / Job)                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  来源:Promise.then/catch/finally, queueMicrotask,  │   │
│  │        MutationObserver, process.nextTick(Node)     │   │
│  │  调度:当前 macrotask 结束后立即清空全部微任务        │   │
│  │  特点:微任务中新增的微任务也会在本轮执行完毕          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  requestAnimationFrame                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  在渲染之前执行,不属于 microtask 也不属于 macrotask   │   │
│  │  执行时机:微任务清空后 → rAF 回调 → 样式计算 → 布局  │   │
│  │            → 绘制 → 下一个 macrotask                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

2.2 经典执行顺序示例 ​

ts
console.log('1: sync')

setTimeout(() => console.log('2: setTimeout'), 0)

Promise.resolve().then(() => console.log('3: Promise.then'))

queueMicrotask(() => console.log('4: queueMicrotask'))

requestAnimationFrame(() => console.log('5: rAF'))

console.log('6: sync end')

// 浏览器典型输出(单次 tick):
// 1: sync
// 6: sync end
// 3: Promise.then
// 4: queueMicrotask
// 5: rAF (可能在下一帧)
// 2: setTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2.3 setTimeout(fn, 0) 并非立即执行 ​

延迟参数是最小等待时间,而非精确时间。setTimeout(fn, 0) 意味着"尽快将此回调放入 macrotask 队列",但:

┌─────────────────────────────────────────────────────────────┐
│          为何 setTimeout(fn, 0) 不立即执行?                 │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原因 1:嵌套层级限制                                        │
│  浏览器对嵌套超过 5 层的 setTimeout 强制最小 4ms 延迟        │
│                                                             │
│  原因 2:微任务优先                                          │
│  当前 macrotask 结束后必须先清空所有微任务                    │
│                                                             │
│  原因 3:渲染管线                                            │
│  浏览器可能在 macrotask 之间插入渲染帧(~16.6ms)            │
│                                                             │
│  原因 4:定时器合并                                          │
│  浏览器可能将相邻的多个定时器合并到同一时机执行               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2.4 Node.js 特殊排序:nextTick vs microtask ​

ts
// Node.js 环境中的优先级
Promise.resolve().then(() => console.log('Promise'))
process.nextTick(() => console.log('nextTick'))
queueMicrotask(() => console.log('queueMicrotask'))

// 输出顺序:
// nextTick        ← process.nextTick 优先级最高
// Promise         ← 然后是 Promise.then
// queueMicrotask  ← queueMicrotask 等同于 Promise.then

// 关键规则:Node.js 在每个事件循环阶段切换前,
// 先清空 nextTick 队列,再清空 microtask 队列
1
2
3
4
5
6
7
8
9
10
11
12

第3部分:Promise 深度解析 ​

3.1 Promise 状态机 ​

Promise 是不可逆的三态状态机:Pending → Fulfilled 或 Pending → Rejected。一旦 settled,状态永久锁定。

┌─────────────────────────────────────────────────────────────┐
│                    Promise 状态机                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                   ┌──────────┐                              │
│                   │  Pending  │                              │
│                   │  (待定)    │                              │
│                   └─────┬─────┘                              │
│                         │                                    │
│            resolve(x)   │   reject(e) / throw                │
│                         │                                    │
│          ┌──────────────┼──────────────┐                    │
│          ▼              │              ▼                    │
│  ┌──────────────┐       │      ┌──────────────┐            │
│  │  Fulfilled   │       │      │   Rejected    │            │
│  │  (已兑现)     │       │      │   (已拒绝)     │            │
│  │  value: x    │       │      │  reason: e    │            │
│  └──────────────┘       │      └──────────────┘            │
│          │              │              │                    │
│          ▼              │              ▼                    │
│    .then(onFulfilled)   │     .then(null, onRejected)       │
│                         │     .catch(onRejected)            │
│                         │                                    │
│  规则:                                                      │
│  • Settled 后状态不可变                                      │
│  • .then() 永远返回一个新的 Promise                          │
│  • 回调返回值决定新 Promise 的状态                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

3.2 Promise 构造函数 ​

ts
// Promise 构造函数的执行是同步的
console.log('A')
new Promise((resolve, reject) => {
  console.log('B: executor 同步执行')
  resolve('done')
})
console.log('C')

// 输出:A → B → C
// executor 立即同步执行,.then 回调才是异步的

// 典型模式:封装回调风格的 API
function readFileAsync(path: string): Promise<string> {
  return new Promise((resolve, reject) => {
    fs.readFile(path, 'utf-8', (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

3.3 链式调用与展平(Flattening) ​

ts
// .then() 返回值自动展平
Promise.resolve('a')
  .then((val) => {
    // 返回普通值 → 包装为 Promise.resolve(value)
    return val + 'b'
  })
  .then((val) => {
    // 返回 Promise → 自动等待其 settled
    return new Promise((resolve) =>
      setTimeout(() => resolve(val + 'c'), 100)
    )
  })
  .then((val) => {
    console.log(val) // 'abc'
  })

// 错误传播:未被捕获的 rejection 沿链向下传播
Promise.resolve()
  .then(() => { throw new Error('oops') })
  .then(() => console.log('不会执行'))
  .catch((err) => console.log('捕获:', err.message))
  .then(() => console.log('catch 之后继续执行'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

3.4 六大静态方法决策树 ​

┌─────────────────────────────────────────────────────────────┐
│              Promise 静态方法选择决策树                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  你有多个 Promise,想要...                                   │
│  │                                                          │
│  ├─ 全部成功才成功,任一失败则失败                            │
│  │  → Promise.all([p1, p2, p3])                            │
│  │                                                          │
│  ├─ 全部 settled 后获取结果(不论成功/失败)                  │
│  │  → Promise.allSettled([p1, p2, p3])                     │
│  │                                                          │
│  ├─ 第一个 settled 的结果(不论成功/失败)                    │
│  │  → Promise.race([p1, p2, p3])                           │
│  │                                                          │
│  ├─ 第一个 fulfilled 的结果,全部失败才 reject               │
│  │  → Promise.any([p1, p2, p3])                            │
│  │                                                          │
│  ├─ 需要在外部 resolve/reject 一个 Promise                   │
│  │  → Promise.withResolvers()  (ES2024)                    │
│  │                                                          │
│  └─ 包装已知值为 Promise                                     │
│     → Promise.resolve(val) / Promise.reject(err)            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

3.5 静态方法对比表 ​

方法输入输出时机失败行为
Promise.allIterable全部 fulfilled任一 rejected 立即 reject
Promise.allSettledIterable全部 settled永不 reject(结果含 status 字段)
Promise.raceIterable第一个 settled第一个是 reject 则 reject
Promise.anyIterable第一个 fulfilled全部 rejected 才抛 AggregateError
Promise.withResolvers无立即返回手动控制
ts
// Promise.allSettled 结果格式
const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/orders'),
  Promise.reject(new Error('bad'))
])
// results[0]: { status: 'fulfilled', value: Response }
// results[1]: { status: 'fulfilled', value: Response }
// results[2]: { status: 'rejected', reason: Error('bad') }

// Promise.any 用于取最快成功的响应
const data = await Promise.any([
  fetch('/api/primary',  { signal: AbortSignal.timeout(500) }),
  fetch('/api/fallback', { signal: AbortSignal.timeout(800) }),
])

// Promise.withResolvers —— 解耦 Promise 创建与控制
const { promise, resolve, reject } = Promise.withResolvers<string>()
// 现在可以把 resolve/reject 传给回调,promise 传给调用方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

3.6 AbortSignal.timeout 与 Promise.withResolvers ​

ts
// AbortSignal.timeout() 简化超时处理(ES2024)
async function fetchWithTimeout(url: string, ms: number) {
  const res = await fetch(url, { signal: AbortSignal.timeout(ms) })
  return res.json()
}

// Promise.withResolvers 实现事件转 Promise
function waitForEvent(target: EventTarget, event: string) {
  const { promise, resolve } = Promise.withResolvers<Event>()
  target.addEventListener(event, resolve, { once: true })
  return promise
}
1
2
3
4
5
6
7
8
9
10
11
12

第4部分:async/await 内部机制 ​

4.1 async 函数本质 ​

async function 是返回 Promise 的语法糖。函数体内的 return 值自动包装为 Promise.resolve(),抛出的异常自动包装为 Promise.reject()。

ts
// 这两种写法等价
async function f1(): Promise<number> {
  return 42
}

function f2(): Promise<number> {
  return Promise.resolve(42)
}
// 两者都返回 Promise<number>,而非 number

// 抛出异常等价
async function g1(): Promise<never> {
  throw new Error('fail')
}

function g2(): Promise<never> {
  return Promise.reject(new Error('fail'))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.2 await 到底做了什么 ​

await 将当前 async 函数的剩余部分作为 .then() 回调来调度。它暂停的是当前 async 函数的执行,而非整个线程。

┌─────────────────────────────────────────────────────────────┐
│                await 的底层等价变换                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  async function example() {                                 │
│    console.log('A')                                         │
│    const result = await fetch('/api')                       │
│    console.log('B', result)                                 │
│    return result                                            │
│  }                                                          │
│                                                             │
│  // 等价于:                                                 │
│  function example() {                                       │
│    console.log('A')                                         │
│    return fetch('/api').then((result) => {                  │
│      console.log('B', result)                               │
│      return result                                          │
│    })                                                       │
│  }                                                          │
│                                                             │
│  关键差异:                                                  │
│  • try/catch 在 async 中可捕获 rejected Promise             │
│  • 调试器可以像同步代码一样逐行调试                           │
│  • 但是 await 后的代码是微任务调度的                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

4.3 错误处理 ​

ts
// async 函数中 try/catch 可捕获 await 的 rejection
async function safeFetch(url: string) {
  try {
    const res = await fetch(url)
    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return await res.json()
  } catch (err) {
    console.error('请求失败:', err)
    return null // 降级处理
  }
}

// 并行操作的错误:每个独立处理
async function loadDashboard() {
  const [user, stats] = await Promise.all([
    safeFetch('/api/user').catch(() => null),
    safeFetch('/api/stats').catch(() => null),
  ])
  return { user, stats }
}

// 常见反模式:不必要的 try/catch 包装
// ❌ 错误
async function bad() {
  try {
    return await fetch('/api')
  } catch (e) {
    throw e  // 空捕获,等效于没有 try/catch
  }
}

// ✅ 正确:直接返回 Promise 让调用方处理
function good(): Promise<Response> {
  return fetch('/api')
}
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

4.4 await 在循环中:串行 vs 并行 ​

ts
// ❌ 串行执行:总耗时 = 每次请求耗时之和
async function fetchSequential(urls: string[]) {
  const results = []
  for (const url of urls) {
    results.push(await fetch(url)) // 每次等待前一个完成
  }
  return results
}

// ✅ 并行执行:总耗时 ≈ 最慢的一次请求
async function fetchParallel(urls: string[]) {
  const promises = urls.map((url) => fetch(url))
  return Promise.all(promises)
}

// ✅ 可控并行:限制并发数(见第7部分)
async function fetchWithLimit(urls: string[], limit: number) {
  const results: Response[] = []
  for (let i = 0; i < urls.length; i += limit) {
    const batch = urls.slice(i, i + limit).map((u) => fetch(u))
    results.push(...(await Promise.all(batch)))
  }
  return results
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

4.5 顶层 await(Top-level await) ​

ts
// ES2022: 模块顶层可直接使用 await(ESM 模块)
// config.ts
const response = await fetch('/config.json')
export const config = await response.json()

// 注意:顶层 await 会阻塞依赖该模块的模块加载
// 仅在确实需要动态初始化时使用
1
2
3
4
5
6
7

第5部分:AbortController 与异步取消 ​

5.1 取消机制全景 ​

┌─────────────────────────────────────────────────────────────┐
│                AbortController 取消链路                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  AbortController                                            │
│  ┌──────────────────────┐                                   │
│  │  .signal: AbortSignal │──→ 传递给 fetch / 自定义操作     │
│  │  .abort(reason?)     │──→ 触发 signal.aborted = true    │
│  └──────────────────────┘                                   │
│           │                                                 │
│           ▼                                                 │
│  AbortSignal                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  .aborted: boolean    — 是否已取消                   │   │
│  │  .reason: any         — 取消原因                     │   │
│  │  .throwIfAborted()   — 若已取消则抛出 reason         │   │
│  │  .onabort: handler   — 取消时触发                    │   │
│  │  addEventListener('abort', fn)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  静态工厂方法:                                              │
│  • AbortSignal.timeout(ms) — 定时自动取消                   │
│  • AbortSignal.any([s1, s2]) — 任一取消则取消               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

5.2 取消 fetch 请求 ​

ts
async function fetchWithTimeout(
  url: string,
  timeoutMs: number
): Promise<Response> {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeoutMs)

  try {
    const response = await fetch(url, { signal: controller.signal })
    return response
  } catch (err) {
    if ((err as Error).name === 'AbortError') {
      throw new Error(`请求超时 (${timeoutMs}ms): ${url}`)
    }
    throw err
  } finally {
    clearTimeout(timer)
  }
}

// 级联取消:用户取消 → 取消所有下游请求
function createCascadeAbort(parentSignal: AbortSignal) {
  const controller = new AbortController()
  // 组合信号:父取消或本地取消任一触发
  const combined = AbortSignal.any([parentSignal, controller.signal])
  // 监听父信号
  parentSignal.addEventListener('abort', () =>
    controller.abort(parentSignal.reason)
  )
  return { signal: combined, controller }
}
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

5.3 将 AbortSignal 集成到自定义异步操作 ​

ts
// 模式1: 轮询检查
async function delay(ms: number, signal?: AbortSignal): Promise<void> {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) return reject(signal.reason)
    const timer = setTimeout(resolve, ms)
    signal?.addEventListener('abort', () => {
      clearTimeout(timer)
      reject(signal.reason)
    }, { once: true })
  })
}

// 模式2: 在循环中主动检查
async function processBatch(
  items: string[],
  signal?: AbortSignal
): Promise<void> {
  for (const item of items) {
    signal?.throwIfAborted()  // ES2024: 已取消则抛出
    await processItem(item)
  }
}

// 模式3: 将 AbortSignal 传递给底层 API
async function fetchLargeFile(url: string, signal?: AbortSignal) {
  const response = await fetch(url, { signal })
  const reader = response.body!.getReader()
  try {
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      signal?.throwIfAborted()
      // 处理 value (Uint8Array)
    }
  } finally {
    reader.releaseLock()
  }
}
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

第6部分:异步迭代器与生成器 ​

6.1 异步迭代器协议 ​

┌─────────────────────────────────────────────────────────────┐
│              同步迭代 vs 异步迭代                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  同步迭代器                 异步迭代器                        │
│  ──────────                 ──────────                       │
│  Symbol.iterator            Symbol.asyncIterator             │
│  → Iterator {              → AsyncIterator {                │
│      next(): {              next(): Promise<{               │
│        value, done           value, done                    │
│      }                        }>                            │
│    }                        }                               │
│                                                             │
│  for (const x of iter)     for await (const x of asyncIter) │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

6.2 异步生成器函数 ​

ts
// 异步生成器:async function* —— 可以 await 的生成器
async function* paginatedFetch<T>(
  baseUrl: string,
  pageSize: number
): AsyncGenerator<T[]> {
  let page = 0
  let hasMore = true
  while (hasMore) {
    const res = await fetch(`${baseUrl}?page=${page}&size=${pageSize}`)
    const data: { items: T[]; hasMore: boolean } = await res.json()
    yield data.items
    hasMore = data.hasMore
    page++
  }
}

// 消费
for await (const batch of paginatedFetch<User>('/api/users', 50)) {
  // 逐批处理,不会一次性加载全部数据
  await processBatch(batch)
}

// 手动迭代 + 取消
async function fetchAllUsers(signal?: AbortSignal) {
  const iter = paginatedFetch<User>('/api/users', 50)
  const all: User[] = []
  while (true) {
    signal?.throwIfAborted()
    const { value, done } = await iter.next()
    if (done) break
    all.push(...value)
  }
  return all
}
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

6.3 ReadableStream 与异步迭代 ​

ts
// ReadableStream 实现了 asyncIterator 协议(ES2023)
async function streamProcess(res: Response) {
  const decoder = new TextDecoder()
  for await (const chunk of res.body!) {
    // chunk 是 Uint8Array
    const text = decoder.decode(chunk, { stream: true })
    console.log('收到:', text)
  }
}

// 自定义 TransformStream + async iteration
function lineSplitter(): TransformStream<Uint8Array, string> {
  let buffer = ''
  const decoder = new TextDecoder()
  return new TransformStream({
    transform(chunk, controller) {
      buffer += decoder.decode(chunk, { stream: true })
      const lines = buffer.split('\n')
      buffer = lines.pop() || ''
      for (const line of lines) {
        controller.enqueue(line)
      }
    },
    flush(controller) {
      if (buffer) controller.enqueue(buffer)
    },
  })
}

// 使用
const response = await fetch('/api/stream')
const lines = response.body!.pipeThrough(lineSplitter())
for await (const line of lines) {
  console.log('行:', line)
}
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

第7部分:并发控制模式 ​

7.1 Semaphore —— 限制并发数 ​

┌─────────────────────────────────────────────────────────────┐
│              Semaphore 并发限制原理                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  并发上限 = 3                                                │
│                                                             │
│  任务队列:[T4, T5, T6, T7, T8, T9...]  ← 等待中            │
│                                                             │
│  执行中:  ┌────┐  ┌────┐  ┌────┐                          │
│            │ T1 │  │ T2 │  │ T3 │  ← 3 个槽位已满           │
│            └────┘  └────┘  └────┘                          │
│              │       │       │                               │
│              ▼       ▼       ▼                               │
│           完成 →  完成 →  完成 → 释放槽位 → T4 进入执行       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts
class Semaphore {
  private permits: number
  private queue: (() => void)[] = []

  constructor(maxConcurrency: number) {
    this.permits = maxConcurrency
  }

  async acquire(): Promise<void> {
    if (this.permits > 0) {
      this.permits--
      return
    }
    return new Promise((resolve) => this.queue.push(resolve))
  }

  release(): void {
    if (this.queue.length > 0) {
      this.queue.shift()!()
    } else {
      this.permits++
    }
  }

  async run<T>(fn: () => Promise<T>): Promise<T> {
    await this.acquire()
    try {
      return await fn()
    } finally {
      this.release()
    }
  }
}

// 使用:最多 3 个并发请求
async function fetchAll(urls: string[], concurrency = 3) {
  const sem = new Semaphore(concurrency)
  return Promise.all(urls.map((url) => sem.run(() => fetch(url))))
}
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

7.2 批量任务调度器 ​

ts
// 通用模式:pMap —— 带并发限制的 Promise.all
async function pMap<T, R>(
  items: T[],
  fn: (item: T, index: number) => Promise<R>,
  concurrency: number = Infinity
): Promise<R[]> {
  const results: R[] = new Array(items.length)
  const sem = new Semaphore(concurrency)

  const tasks = items.map((item, i) =>
    sem.run(async () => {
      results[i] = await fn(item, i)
    })
  )

  await Promise.all(tasks)
  return results
}

// 使用示例:批量处理 100 个 URL,每次最多 5 个
const data = await pMap(
  urls,
  async (url, i) => {
    console.log(`处理第 ${i} 个`)
    return fetch(url).then((r) => r.json())
  },
  5
)
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

7.3 速率限制(Rate Limiting) ​

ts
// Token Bucket 速率限制器
class RateLimiter {
  private tokens: number
  private lastRefill: number
  private readonly maxTokens: number
  private readonly refillRate: number // tokens per ms

  constructor(requestsPerSecond: number, burst?: number) {
    this.maxTokens = burst ?? requestsPerSecond
    this.tokens = this.maxTokens
    this.lastRefill = Date.now()
    this.refillRate = requestsPerSecond / 1000
  }

  async wait(): Promise<void> {
    this.refill()
    if (this.tokens >= 1) {
      this.tokens--
      return
    }
    const waitMs = Math.ceil((1 - this.tokens) / this.refillRate)
    await new Promise((r) => setTimeout(r, waitMs))
    this.tokens = 0
  }

  private refill(): void {
    const now = Date.now()
    const elapsed = now - this.lastRefill
    this.tokens = Math.min(
      this.maxTokens,
      this.tokens + elapsed * this.refillRate
    )
    this.lastRefill = now
  }
}
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

7.4 超时 + 重试 + 退避 ​

ts
async function fetchWithRetry(
  url: string,
  options: {
    retries?: number
    baseDelayMs?: number
    maxDelayMs?: number
    signal?: AbortSignal
  } = {}
): Promise<Response> {
  const { retries = 3, baseDelayMs = 1000, maxDelayMs = 30000, signal } = options

  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, { signal })
      if (res.ok) return res
      // 5xx 才重试,4xx 不重试
      if (res.status < 500 || attempt === retries) return res
    } catch (err) {
      if (attempt === retries || signal?.aborted) throw err
    }
    // 指数退避 + 随机抖动
    const delay = Math.min(
      baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
      maxDelayMs
    )
    await new Promise((r) => setTimeout(r, delay))
  }
  throw new Error('unreachable')
}
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

第8部分:常见异步陷阱 ​

8.1 Floating Promise(遗忘 await) ​

ts
// ❌ 错误:Promise 没有被 await,错误被静默吞掉
async function handleClick() {
  saveToDatabase(data) // 返回 Promise,但未 await!
  redirectTo('/dashboard') // 立即跳转,save 可能未完成
}

// ✅ 正确:要么 await,要么显式处理
async function handleClick() {
  await saveToDatabase(data)
  redirectTo('/dashboard')
}

// ✅ 备选:fire-and-forget 时也要处理错误
function handleClick() {
  saveToDatabase(data).catch((err) => {
    console.error('保存失败', err)
    showErrorToast('保存失败,请重试')
  })
  redirectTo('/dashboard')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

8.2 async forEach 陷阱 ​

ts
// ❌ 错误:forEach 不回等待 async 回调
async function processAll(items: string[]) {
  items.forEach(async (item) => {
    await processItem(item) // 这些 Promise 全被丢弃!
  })
  console.log('done') // 在所有 processItem 完成之前就打印了
}

// ✅ 方案1:for...of 循环
async function processAll(items: string[]) {
  for (const item of items) {
    await processItem(item)
  }
  console.log('done') // 所有处理完成后才打印
}

// ✅ 方案2:并行 + Promise.all
async function processAll(items: string[]) {
  await Promise.all(items.map((item) => processItem(item)))
  console.log('done')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

8.3 Promise 链中忘记 return ​

ts
// ❌ 错误:then 中没有 return,导致链断裂
fetch('/api/user')
  .then((res) => {
    res.json() // 应该 return res.json()!
  })
  .then((user) => {
    console.log(user) // undefined!
  })

// ✅ 正确:必须 return
fetch('/api/user')
  .then((res) => res.json())
  .then((user) => console.log(user))

// ✅ 或使用 async/await 彻底避免此问题
async function getUser() {
  const res = await fetch('/api/user')
  return res.json()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

8.4 异步竞态条件 ​

ts
// ❌ 错误:竞态条件 —— 旧请求可能覆盖新请求
let lastQuery = ''
async function search(query: string) {
  lastQuery = query
  const results = await fetch(`/api/search?q=${query}`)
  if (query !== lastQuery) return // 过期结果,丢弃!
  renderResults(await results.json())
}

// ✅ 使用 AbortController 取消旧请求
let abortController: AbortController | null = null
async function search(query: string) {
  abortController?.abort()
  abortController = new AbortController()
  try {
    const results = await fetch(`/api/search?q=${query}`, {
      signal: abortController.signal,
    })
    renderResults(await results.json())
  } catch (err) {
    if ((err as Error).name !== 'AbortError') throw err
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

8.5 Promisify 陷阱 ​

ts
// ❌ 错误:忘记处理 error 事件
function badPromisify(stream: EventEmitter): Promise<Buffer> {
  return new Promise((resolve) => {
    const chunks: Buffer[] = []
    stream.on('data', (chunk) => chunks.push(chunk))
    stream.on('end', () => resolve(Buffer.concat(chunks)))
    // 缺少 stream.on('error', reject)!
  })
}

// ✅ 正确:完整的事件处理
function goodPromisify(stream: EventEmitter): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = []
    stream.on('data', (chunk) => chunks.push(chunk))
    stream.on('end', () => resolve(Buffer.concat(chunks)))
    stream.on('error', reject)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

8.6 混用同步与异步的控制流 ​

ts
// ❌ 陷阱:条件分支中混用 async
function getConfig(): Config | Promise<Config> {
  if (cachedConfig) {
    return cachedConfig // 同步返回
  }
  return fetch('/config').then((r) => r.json()) // 异步返回
}
// 调用者不知道该 await 还是直接用!

// ✅ 始终返回 Promise
async function getConfig(): Promise<Config> {
  if (cachedConfig) return cachedConfig
  const res = await fetch('/config')
  cachedConfig = await res.json()
  return cachedConfig
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

8.7 Unhandled Rejection 与全局监听 ​

ts
// 未处理的 Promise rejection 会导致 Node.js 进程崩溃
// 但不会像同步异常那样立即崩溃

// 全局捕获(防御性编程)
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise rejection:', reason)
  // 记录日志、发送告警,但不要阻止进程退出
})

// 浏览器端
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 rejection:', event.reason)
  event.preventDefault() // 阻止默认的控制台错误
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第9部分:Streams API 概述 ​

9.1 三种核心流类型 ​

┌─────────────────────────────────────────────────────────────┐
│                    Streams API 核心概念                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ReadableStream<T>          WritableStream<T>               │
│  ┌──────────────────┐      ┌──────────────────┐            │
│  │ reader.read()     │      │ writer.write()    │            │
│  │      ↓            │      │      ↓            │            │
│  │ { value, done }   │      │ writer.close()    │            │
│  │ controller.enqueue│      │ controller.error  │            │
│  └──────────────────┘      └──────────────────┘            │
│           │                         ▲                       │
│           │    ┌──────────────┐     │                       │
│           └───→│TransformStream│────┘                       │
│                │ readable      │                             │
│                │ writable      │                             │
│                │ .transform()  │                             │
│                └──────────────┘                             │
│                                                             │
│  关键方法:                                                  │
│  • readable.pipeThrough(transform) → ReadableStream         │
│  • readable.pipeTo(writable) → Promise<void>                │
│  • readable.tee() → [ReadableStream, ReadableStream]        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
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

9.2 fetch response.body 流式读取 ​

ts
// 以 ReadableStream 方式读取大文件
async function downloadProgressively(url: string) {
  const response = await fetch(url)
  const contentLength = Number(response.headers.get('Content-Length')) || 0
  const reader = response.body!.getReader()
  let received = 0
  const chunks: Uint8Array[] = []

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    chunks.push(value)
    received += value.length
    console.log(`进度: ${((received / contentLength) * 100).toFixed(1)}%`)
  }

  return new Blob(chunks)
}

// tee() 分支:一份数据两个消费者(如边下载边 hash)
async function downloadAndHash(url: string) {
  const response = await fetch(url)
  const [stream1, stream2] = response.body!.tee()

  // 分支1:保存到文件
  const savePromise = saveToFile(stream1)

  // 分支2:计算 SHA-256
  const hashPromise = computeSHA256(stream2)

  const [path, hash] = await Promise.all([savePromise, hashPromise])
  return { path, hash }
}
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

9.3 自定义 ReadableStream ​

ts
// 生成一个"计时器流",每秒产生一个时间戳
function createTimerStream(count: number): ReadableStream<string> {
  let emitted = 0
  return new ReadableStream({
    async pull(controller) {
      if (emitted >= count) {
        controller.close()
        return
      }
      await new Promise((r) => setTimeout(r, 1000))
      controller.enqueue(new Date().toISOString())
      emitted++
    },
  })
}

for await (const timestamp of createTimerStream(5)) {
  console.log('滴答:', timestamp)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

核心总结 ​

1. 事件循环是 JavaScript 异步的心脏 ​

事件循环遵循"一个 macrotask → 清空所有 microtask → 可能渲染 → 下一个 macrotask"的核心循环。理解这个模型是写出正确异步代码的前提。Node.js 的 libuv 六阶段模型是其变体,process.nextTick 在每个阶段切换前优先执行。

2. Promise 是构建异步代码的基石 ​

Promise 是不可逆的三态状态机。.then() 永远返回新 Promise,错误沿链传播。选择合适的静态方法(all、allSettled、race、any)是高效并发编程的关键。async/await 让异步代码看起来像同步代码,但本质仍是 Promise + 微任务调度。

3. 并发控制与取消是生产系统的必备能力 ​

Promise.all 对大量任务不设限可能耗尽连接和内存。Semaphore 模式提供可控并发,AbortController 提供级联取消。AbortSignal.timeout() 和 AbortSignal.any() 使超时和组合取消变得简单。异步迭代器和 Streams API 让流式数据处理有了标准化方案。


章节测试 ​

Q1: 以下代码的输出顺序是?

ts
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
queueMicrotask(() => {
  console.log('4')
  Promise.resolve().then(() => console.log('5'))
})
console.log('6')
1
2
3
4
5
6
7
8

A) 1, 6, 3, 4, 2, 5 B) 1, 6, 3, 4, 5, 2 C) 1, 2, 3, 4, 5, 6 D) 1, 6, 4, 3, 5, 2

Q2: Promise.all 与 Promise.allSettled 的核心区别是什么?各自适合什么场景?

Q3: 以下代码有什么问题?如何修复?

ts
async function process(items: number[]) {
  items.forEach(async (item) => {
    await save(item)
  })
  console.log('all done')
}
1
2
3
4
5
6

Q4: async function 的返回值类型是什么?为什么 async function f() { return 42 } 的返回类型是 Promise<number> 而非 number?

Q5: Node.js 中 process.nextTick、Promise.then、setImmediate 三者的执行顺序是怎样的?

Q6: 如何用 AbortController 实现一个 5 秒超时的 fetch,且超时后不要留下未清理的 timer?

Q7: 补全以下代码,实现最多 3 个并发请求的批量下载功能:

ts
async function downloadAll(urls: string[]): Promise<Response[]> {
  // 你的实现
}
1
2
3

参考答案 ​

A1: B。同步代码先执行 (1, 6);然后是微任务清空阶段:先 3 (Promise.then),再 4 (queueMicrotask),queueMicrotask 内部新增的 5 也在同一轮清空;最后是宏任务 2 (setTimeout)。

A2: Promise.all 在任一输入 reject 时立即 reject(fail-fast),适合"全部成功才有意义"的场景。Promise.allSettled 等待所有输入 settled,结果数组包含 {status, value/reason},适合"尽可能多地获取结果,容忍部分失败"的场景。

A3: forEach 不会等待 async 回调,'all done' 在所有 save() 完成前就打印了。修复:使用 for...of 循环逐次 await,或 await Promise.all(items.map(save)) 并行执行。

A4: async function 始终返回 Promise。return 42 等价于 return Promise.resolve(42)。调用方需要 .then() 或 await 才能拿到 42。这是 async 函数的核心约定。

A5: 执行顺序为:process.nextTick (最高优先) → Promise.then / queueMicrotask → setImmediate (下一轮事件循环的 check 阶段)。nextTick 在每个阶段切换前执行,microtask 在 nextTick 之后执行,setImmediate 在 poll 阶段之后的 check 阶段执行。

A6:

ts
async function fetchWith5sTimeout(url: string) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 5000)
  try {
    return await fetch(url, { signal: controller.signal })
  } finally {
    clearTimeout(timer) // 成功或失败都清理 timer
  }
}
1
2
3
4
5
6
7
8
9

A7: 参见第7部分的 pMap 或 Semaphore 实现,核心是控制并发数为 3。


相关笔记 ​

  • [[01-javascript-runtime-and-language]] — JavaScript 运行时环境、V8 引擎、内存模型
  • [[02-typescript-type-system]] — TypeScript 中 Promise 泛型标注与类型推导
  • [[00-overview]] — JavaScript 与 TypeScript 章节总览

下一步学习 ​

  • [ ] 在浏览器 DevTools 的 Performance 面板中录制并观察事件循环的行为
  • [ ] 手写一个 Semaphore 类并在实际项目中使用它控制并发
  • [ ] 将项目中所有的"遗忘 await"和"async forEach"问题修复
  • [ ] 用 AbortController 改造现有请求管理,实现级联取消
  • [ ] 尝试用 ReadableStream 和 TransformStream 处理流式数据

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇3. TypeScript 类型系统:从结构类型到类型编程 / The TypeScript Type System from Structural Typing to Type-Level Programming

持续记录,持续成长

Copyright © Tidenflow