JavaScript 异步编程:从回调到 async/await

2024-09-02 前端

JavaScript 的异步编程模型经历了从回调函数到 Promise,再到 async/await 的演进。但无论语法如何变化,底层的事件循环(Event Loop)始终是理解一切异步行为的关键。本文将从事件循环出发,逐步深入每个异步编程模式,覆盖所有核心知识点,并配有大量可运行的代码示例。


一、事件循环(Event Loop)

JavaScript 是单线程语言,同一时间只能执行一个任务。事件循环机制让它能够通过将耗时操作交给宿主环境(浏览器/Node.js)处理,在等待结果的同时继续执行其他代码,从而实现非阻塞 I/O。

1.1 调用栈(Call Stack)

调用栈是一个 LIFO(后进先出)的数据结构,记录当前正在执行的函数。当函数被调用时,它的执行上下文被推入栈顶;函数返回时,上下文被弹出。

function a() {
  console.log('a');
  b();
  console.log('a end');
}
function b() {
  console.log('b');
  c();
  console.log('b end');
}
function c() {
  console.log('c');
}

a();
// 输出顺序:
// a
// b
// c
// b end
// a end

调用栈的进出过程:

  1. a() 入栈 → 输出 a → b() 入栈
  2. 输出 b → c() 入栈
  3. 输出 c → c() 出栈 → 输出 b end → b() 出栈
  4. 输出 a end → a() 出栈 → 栈空

当调用栈超出最大深度(通常约 10000 层)时会抛出 RangeError: Maximum call stack size exceeded(即栈溢出)。

function infinite() {
  return infinite();
}
infinite(); // RangeError: Maximum call stack size exceeded

1.2 任务队列(Task Queue / Macrotask)

当调用栈清空后,事件循环会从宏任务队列中取出第一个任务放入调用栈执行。宏任务包括:

  • setTimeout / setInterval
  • setImmediate(Node.js)
  • I/O 回调(文件读写、网络请求等)
  • UI 事件(点击、键盘等)
  • postMessage、MessageChannel

每次事件循环迭代(一次 tick)处理一个宏任务。

1.3 微任务队列(Microtask Queue)

微任务队列在每个宏任务执行完毕后、下一个宏任务开始前被清空。微任务包括:

  • Promise.then / catch / finally
  • MutationObserver
  • queueMicrotask
  • process.nextTick(Node.js,优先级高于 Promise)
console.log('1'); // 同步

setTimeout(() => console.log('2'), 0); // 宏任务

Promise.resolve().then(() => console.log('3')); // 微任务

queueMicrotask(() => console.log('4')); // 微任务

console.log('5'); // 同步

// 输出: 1 5 3 4 2
// 同步代码 → 清空微任务 → 宏任务

1.4 完整执行顺序

一次事件循环 tick 的执行顺序:

执行同步脚本(script 宏任务)
  → 清空微任务队列
  → 必要时更新渲染(UI)
  → 取下一个宏任务执行
  → 清空微任务队列
  → ...

规则总结:script → 清空微任务 → 渲染(可能) → 宏任务 → 清空微任务 → 渲染(可能) → ...

1.5 经典面试题

console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => console.log('3'));
}, 0);

Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => console.log('5'), 0);
});

queueMicrotask(() => console.log('6'));

console.log('7');

// 分析:
// 同步: 1, 7
// 微任务队列: 4 → 注册 setTimeout(5) → 6
// 微任务清空后: 宏任务 2 → 输出 2 → 注册微任务 3 → 清空微任务 3 → 宏任务 5
// 最终: 1, 7, 4, 6, 2, 3, 5

1.6 requestAnimationFrame 在事件循环中的位置

requestAnimationFrame(RAF)的执行时机在微任务清空之后、渲染之前。它不属于宏任务或微任务,而是与浏览器渲染管线绑定:

宏任务 → 清空微任务 → RAF 回调 → 渲染 → 下一个宏任务
button.addEventListener('click', () => {
  requestAnimationFrame(() => {
    console.log('RAF after click');
  });
  Promise.resolve().then(() => console.log('micro after click'));
  console.log('sync in click');
});

// 输出: sync in click → micro after click → RAF after click

由于 RAF 在渲染前执行,适合在此修改 DOM 或执行动画计算,避免掉帧。

1.7 Node.js 事件循环(六个阶段)

Node.js 的事件循环分为六个阶段,每个阶段都有一个 FIFO 回调队列:

   ┌───────────────────────────┐
┌─>│         timers            │ ← setTimeout/setInterval 到期回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │ ← 上一轮延迟的 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │ ← 内部使用
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │         poll              │ ← 轮询 I/O,执行 I/O 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │        check              │ ← setImmediate 回调
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │    close callbacks        │ ← close 事件回调(如 socket.destroy)
│  └───────────────────────────┘
└───────────────────────────────┘

两个关键点:

  • process.nextTick 不在任何阶段中,它在每个阶段切换时执行,优先级高于微任务。
  • 微任务(Promise.then)在每个阶段之后执行。
// Node.js 中:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 在 top-level 中执行顺序不确定(受进程启动时间影响)
// 在 I/O 回调中,setImmediate 始终先于 setTimeout

const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => console.log('timeout'));
  setImmediate(() => console.log('immediate'));
});
// 输出: immediate → timeout

二、回调函数

2.1 同步回调 vs 异步回调

同步回调:在调用函数的过程中直接执行,阻塞后续代码。

function map(arr, callback) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(callback(arr[i], i, arr));
  }
  return result;
}

const doubled = map([1, 2, 3], x => x * 2);
console.log(doubled); // [2, 4, 6]

异步回调:注册后等待某个事件触发再执行,不阻塞后续代码。

console.log('start');

setTimeout(() => {
  console.log('async callback');
}, 1000);

console.log('end');
// start → end → (1秒后) async callback

2.2 错误优先回调模式(Error-First Callback)

Node.js 的惯用模式:回调的第一个参数是错误对象(若无错误则为 null)。

const fs = require('fs');

fs.readFile('/path/to/file', 'utf-8', (err, data) => {
  if (err) {
    console.error('读取失败:', err.message);
    return;
  }
  console.log('文件内容:', data);
});

2.3 回调地狱(Callback Hell)

当多个异步操作有依赖关系时,回调嵌套会急剧增加,形成"回调地狱"或"末日金字塔":

getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getOrderDetails(orders[0].id, (err, details) => {
      if (err) return handleError(err);
      getPayment(details.paymentId, (err, payment) => {
        if (err) return handleError(err);
        console.log('支付信息:', payment);
      });
    });
  });
});

回调地狱的问题:

  1. 可读性差:嵌套层次深,逻辑难以追踪
  2. 错误处理重复:每层都需要 if (err) 判断
  3. 难以复用:回调函数与上下文强耦合
  4. 控制流困难:并行、超时、重试等场景复杂

2.4 回调地狱的解决方案演进

  1. 具名函数:将回调拆分为具名函数(减轻嵌套,但未解决根本问题)
function onPayment(err, payment) {
  if (err) return handleError(err);
  console.log('支付信息:', payment);
}
function onDetails(err, details) {
  if (err) return handleError(err);
  getPayment(details.paymentId, onPayment);
}
function onOrders(err, orders) {
  if (err) return handleError(err);
  getOrderDetails(orders[0].id, onDetails);
}
getUser(id, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, onOrders);
});
  1. async 工具库(如 async.js 的 waterfall、parallel)

  2. Promise(最终方案,见下一章)


三、Promise

Promise 是 ES6 引入的内置异步编程方案,它代表一个未来可能完成或失败的值,彻底解决了回调地狱的问题。

3.1 状态机

Promise 有三种状态,一旦改变就不可逆:

pending (待定) ──→ fulfilled (已完成)
       │
       └──→ rejected (已拒绝)
const promise = new Promise((resolve, reject) => {
  // pending 状态
  setTimeout(() => {
    resolve('success'); // 变为 fulfilled
    // resolve('again'); // 无效,状态已锁定
    // reject('err');    // 无效
  }, 1000);
});

3.2 构造函数

new Promise(executor),executor 立即执行(同步执行):

console.log('1');
const p = new Promise((resolve, reject) => {
  console.log('2');          // 同步执行
  resolve('3');
});
console.log('4');
p.then(v => console.log(v)); // 异步(微任务)
console.log('5');
// 输出: 1 2 4 5 3

3.3 .then()、.catch()、.finally()

  • .then(onFulfilled, onRejected):注册成功/失败回调
  • .catch(onRejected):注册失败回调,等价于 .then(null, onRejected)
  • .finally(onFinally):无论成功或失败都会执行,不接收状态值
const p = new Promise((resolve, reject) => {
  // resolve('ok');
  reject('fail');
});

p.then(
  value => console.log('成功:', value),
  reason => console.log('失败:', reason)
);

// 更推荐的写法(链式分离):
p.then(value => console.log('成功:', value))
 .catch(reason => console.log('失败:', reason))
 .finally(() => console.log('总是执行'));

3.4 链式调用与值的传递

.then() 和 .catch() 返回一个新的 Promise,因此可以链式调用。返回值规则:

回调返回值 下一个 then 接收
普通值(含 undefined) resolved 为该值
Promise 对象 跟随该 Promise 状态
throw / rejected Promise rejected 为错误
thenable 对象(有 then 方法) 展开为 Promise
Promise.resolve(1)
  .then(v => v + 1)          // 返回 2
  .then(v => v * 2)          // 返回 4
  .then(v => Promise.resolve(v + 1)) // 返回 Promise(5)
  .then(console.log)         // 5
  .then(() => { throw new Error('oops'); })
  .catch(err => {
    console.error(err.message); // 'oops'
    return 'recovered';
  })
  .then(v => console.log(v)); // 'recovered'

3.5 错误处理

reject vs throw:在 executor 中,reject() 和 throw 效果相同,但 throw 可能被 try/catch 捕获。

// 方式一:reject
new Promise((_, reject) => reject(new Error('fail')));

// 方式二:throw
new Promise((resolve, reject) => {
  throw new Error('fail'); // 自动触发 reject
});

// 注意:非 Error 对象的 throw
new Promise(() => {
  throw 'string error'; // 也会被 catch 捕获,但建议总是 throw Error 对象
}).catch(err => console.log(err)); // 'string error'

未捕获的 rejection:如果没有 .catch(),Promise 拒绝时不会抛出错误(浏览器会打印警告,Node.js v15+ 会直接报错退出):

// 危险!未捕获的 rejection
Promise.reject('oops'); // UnhandledPromiseRejection

// 安全做法:全局监听(浏览器)
window.addEventListener('unhandledrejection', event => {
  console.error('未处理的 rejection:', event.reason);
  event.preventDefault();
});

// Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 rejection:', reason);
});

3.6 Promise 静态方法

Promise.resolve / Promise.reject

快速创建已决定状态的 Promise:

Promise.resolve(42);              // 等价于 new Promise(r => r(42))
Promise.reject(new Error('bad')); // 等价于 new Promise((_, r) => r(new Error('bad')))

// Promise.resolve 的 thenable 展开特性
const thenable = { then: (resolve) => resolve('from thenable') };
Promise.resolve(thenable).then(console.log); // 'from thenable'

Promise.all(全部成功 or 一个失败)

所有 Promise 都成功则返回结果数组,任何一个拒绝则立即拒绝。

const p1 = Promise.resolve(1);
const p2 = new Promise(r => setTimeout(() => r(2), 100));
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3]).then(results => {
  console.log(results); // [1, 2, 3] (保持传入顺序)
});

// 失败情况
Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error('fail')),
  Promise.resolve(3)
]).catch(err => console.log(err.message)); // 'fail' (立即拒绝,3 的结果被忽略)

注意:空数组 Promise.all([]) 返回 []。

Promise.allSettled(等待所有完成,不关心结果)

所有 Promise 完成后返回每个 Promise 的结果对象({status, value/reason})。

Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(new Error('fail')),
  Promise.resolve(3)
]).then(results => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: Error: fail },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

适用于"即使部分失败也要知道所有结果"的场景。

Promise.race(第一个完成)

返回第一个状态确定的 Promise(无论是 fulfilled 还是 rejected)。

const slow = new Promise(r => setTimeout(() => r('slow'), 1000));
const fast = new Promise(r => setTimeout(() => r('fast'), 100));

Promise.race([slow, fast]).then(console.log); // 'fast' (100ms 后)

// 超时控制示例
function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('timeout')), ms)
  );
}

function fetchWithTimeout(url, ms) {
  return Promise.race([fetch(url), timeout(ms)]);
}

注意:空数组 Promise.race([]) 永远 pending。

Promise.any(第一个成功)

返回第一个 fulfilled 的 Promise,如果全部 rejected 则返回 AggregateError。

const p1 = Promise.reject(new Error('1'));
const p2 = new Promise(r => setTimeout(() => r('success'), 100));
const p3 = Promise.reject(new Error('3'));

Promise.any([p1, p2, p3]).then(console.log); // 'success'

// 全失败时
Promise.any([
  Promise.reject('a'),
  Promise.reject('b')
]).catch(err => {
  console.log(err instanceof AggregateError); // true
  console.log(err.errors); // ['a', 'b']
});

注意:空数组 Promise.any([]) 返回 rejected 的 AggregateError。

3.7 Promise 并发控制(限制同时执行数)

3.8 常见陷阱

陷阱 1:then 中返回 Promise 时忘记 return

// ❌ 错误
Promise.resolve(1)
  .then(() => {
    Promise.resolve(2); // 没有 return!返回 undefined
  })
  .then(v => console.log(v)); // undefined

// ✅ 正确
Promise.resolve(1)
  .then(() => Promise.resolve(2))
  .then(v => console.log(v)); // 2

陷阱 2:在 then 中嵌套 Promise

// ❌ 嵌套地狱(回到回调模式)
fetch('/api/user')
  .then(res => res.json())
  .then(user => {
    fetch(`/api/orders/${user.id}`)
      .then(res => res.json())
      .then(orders => {
        console.log(orders);
      });
  });

// ✅ 链式展开
fetch('/api/user')
  .then(res => res.json())
  .then(user => fetch(`/api/orders/${user.id}`))
  .then(res => res.json())
  .then(console.log);

陷阱 3:forEach 中 Promise 不会等待

// ❌ forEach 不会等待 Promise
[1, 2, 3].forEach(async (id) => {
  const data = await fetch(`/api/${id}`);
  console.log(data);
});
console.log('Done'); // 先输出!

// ✅ 使用 Promise.all + map
await Promise.all([1, 2, 3].map(async (id) => {
  const data = await fetch(`/api/${id}`);
  console.log(data);
}));
console.log('Done');

四、async/await

async/await 是 ES2017 引入的语法糖,它让异步代码看起来像同步代码,本质仍然是 Promise。

4.1 async 函数

async 函数总是返回一个 Promise。如果返回值不是 Promise,会被 Promise.resolve() 包裹。

async function foo() {
  return 42;
}
// 等价于:
// function foo() { return Promise.resolve(42); }

foo().then(console.log); // 42

// async 函数内部 throw 等价于 reject
async function bar() {
  throw new Error('oops');
}
bar().catch(err => console.log(err.message)); // 'oops'

4.2 await 表达式

await 等待一个 Promise 完成,会暂停当前 async 函数的执行(不阻塞事件循环),直到 Promise 状态确定。

function delay(ms) {
  return new Promise(r => setTimeout(r, ms));
}

async function demo() {
  console.log('开始');
  await delay(1000);
  console.log('1秒后');
  await delay(500);
  console.log('再500毫秒后');
  return '完成';
}

demo().then(console.log);

await 可以等待任意值 —— 如果值不是 Promise,则自动转为 Promise.resolve(value):

async function example() {
  const a = await 42;          // 等同 await Promise.resolve(42)
  const b = await 'hello';     // 等同 await Promise.resolve('hello')
  console.log(a, b);           // 42 'hello'
}

4.3 try/catch 错误处理

await 的 Promise 拒绝时,可以使用 try/catch 捕获:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error('请求失败:', error.message);
    return null; // 降级处理
  }
}

也可以不捕获,让 Promise 链的 .catch() 处理:

async function risky() {
  return await Promise.reject('bad');
}

risky().catch(err => console.error(err)); // 'bad'

注意:多个 await 共享一个 try/catch 时,任何一个失败都会跳到 catch:

async function processAll() {
  try {
    const a = await step1();  // 如果这里失败
    const b = await step2(a); // step2 不会执行
    return await step3(b);
  } catch (error) {
    console.error('任意步骤失败:', error);
  }
}

如果希望各自独立处理错误,使用各自的 try/catch 块。

4.4 并行:await + Promise.all

串行 await 很慢,无依赖的任务应该并行执行:

// ❌ 串行(慢!)
async function serial() {
  const user = await fetch('/api/user').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());
  const comments = await fetch('/api/comments').then(r => r.json());
  return { user, posts, comments };
}
// 总耗时 = 最慢请求 × 3

// ✅ 并行(快!)
async function parallel() {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json()),
  ]);
  return { user, posts, comments };
}
// 总耗时 = 最慢请求(三者同时发起)

4.5 串行 vs 并行

场景 推荐方式 原因
A 依赖 B const a = await A(); const b = await B(a); 串行
A 和 B 无依赖 const [a, b] = await Promise.all([A(), B()]); 并行
批量但需限流 parallelLimit(tasks, n); 并行 + 限流
依次处理(如逐行写入) for (const item of items) await process(item); 串行

4.6 async/await 在 forEach/map/reduce 中的行为差异

forEach

forEach 不处理 Promise,await 在 forEach 回调中无效:

// ❌ forEach 无法等待
async function badLoop() {
  const ids = [1, 2, 3];
  ids.forEach(async (id) => {
    const data = await fetch(`/api/${id}`);
    console.log(data);
  });
  console.log('forEach done'); // 立即输出
}

// ✅ 用 for...of 实现串行
async function goodSerialLoop() {
  const ids = [1, 2, 3];
  for (const id of ids) {
    const data = await fetch(`/api/${id}`);
    console.log(data);
  }
  console.log('全部串行完成');
}

map

map 返回一个 Promise 数组,需要用 Promise.all 等待:

// ✅ 并行 map
async function parallelMap() {
  const ids = [1, 2, 3];
  const results = await Promise.all(
    ids.map(async (id) => {
      const res = await fetch(`/api/${id}`);
      return res.json();
    })
  );
  console.log(results); // 三个结果并行获取
}

reduce

reduce 可以用于串行链式调用(每次等待上一次的结果):

// ✅ reduce 实现串行
async function serialReduce() {
  const tasks = [1, 2, 3, 4, 5];

  const result = await tasks.reduce(async (prevPromise, current) => {
    const acc = await prevPromise;
    const value = await new Promise(r =>
      setTimeout(() => r(acc + current), 500)
    );
    console.log(`当前: ${value}`);
    return value;
  }, Promise.resolve(0));

  console.log('最终结果:', result); // 15
  // 每 500ms 输出一个累积值
}

4.7 顶层 await(ES2022)

在模块的顶层可以直接使用 await,无需包裹在 async 函数中。模块会等待顶层 await 完成后再执行依赖它的模块。

// db.mjs
export const db = await connectToDatabase();
console.log('数据库连接成功');

// app.mjs(依赖 db)
import { db } from './db.mjs';
// 等待 db.mjs 的顶层 await 完成后才执行
console.log('开始使用数据库');

注意:顶层 await 只在 ES Module(type: "module" 或 .mjs 文件)中可用,CommonJS 不支持。

4.8 避免的陷阱:for 循环中的 await 性能问题

// ❌ 串行太慢(如果任务无依赖)
async function processAllBad(items) {
  for (const item of items) {
    await heavyIO(item); // 一个一个等待
  }
}

// ✅ 并行(如果任务无依赖且内存允许)
async function processAllGood(items) {
  await Promise.all(items.map(item => heavyIO(item)));
}

// ✅ 并行 + 限流(最佳实践)
async function processAllBatched(items, limit = 5) {
  const results = [];
  for (let i = 0; i < items.length; i += limit) {
    const batch = items.slice(i, i + limit);
    const batchResults = await Promise.all(
      batch.map(item => heavyIO(item))
    );
    results.push(...batchResults);
  }
  return results;
}

五、定时器

5.1 setTimeout / setInterval / setImmediate

// setTimeout: 延迟执行(一次)
const timer1 = setTimeout(() => {
  console.log('延迟 1 秒');
}, 1000);

// 带参数
setTimeout((a, b) => {
  console.log(a + b); // 3
}, 100, 1, 2);

// setInterval: 重复执行
const timer2 = setInterval(() => {
  console.log('每秒一次');
  clearInterval(timer2); // 取消
}, 1000);

// setImmediate: 立即执行(Node.js,在 check 阶段)
// setTimeout(fn, 0) 在 timers 阶段,setImmediate 在 check 阶段
const timer3 = setImmediate(() => {
  console.log('Node.js setImmediate');
});
clearImmediate(timer3);

5.2 最小延迟与嵌套限制

浏览器对 setTimeout 有嵌套延迟限制:

  • 最小延迟:4ms(HTML 规范规定)
  • 嵌套超时:第 5 层嵌套后,最小延迟变为 4ms(之前可以是 0)
let count = 0;
const start = Date.now();

function nested() {
  count++;
  if (count <= 5) {
    const now = Date.now();
    console.log(`第 ${count} 次: ${now - start}ms`);
    setTimeout(nested, 0);
  }
}

setTimeout(nested, 0);
// 输出可能类似:
// 第 1 次: 0ms
// 第 2 次: 1ms
// 第 3 次: 2ms
// 第 4 次: 3ms
// 第 5 次: 8ms (超过 4ms 限制)

5.3 零延迟 setTimeout 的真实含义

setTimeout(fn, 0) 并不是立即执行,而是将回调放入宏任务队列,等待当前同步代码和微任务队列清空后才执行。

console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// A D C B (B 在最后)

5.4 requestAnimationFrame 动画定时

requestAnimationFrame 是浏览器专用的动画帧定时器,跟随屏幕刷新率(通常 60Hz/120Hz),在渲染前执行:

function animate() {
  element.style.transform = `translateX(${pos}px)`;
  pos += 1;
  if (pos < 300) {
    requestAnimationFrame(animate); // 下次重绘前调用
  }
}

let pos = 0;
requestAnimationFrame(animate);
与 setInterval 相比: 特性 setInterval requestAnimationFrame
执行时机 固定延迟 同步刷新率
页面不可见时 继续执行(后台耗能) 暂停(节省资源)
帧同步 可能丢帧或卡顿 每次渲染前执行
精度 4ms 最小延迟 与帧率同步

5.5 用 AbortSignal 取消定时器

现代浏览器支持通过 AbortSignal 取消定时器(无需保存 timer ID):

// 使用 AbortController 取消 setTimeout
const controller = new AbortController();
const { signal } = controller;

setTimeout(() => {
  console.log('这条不会执行 (被取消)');
}, 1000, { signal }); // 第三个参数传 options

controller.abort(); // 取消定时器

// 检查是否被取消
console.log(signal.aborted); // true

六、并发模式实战

6.1 并发请求限制(Promise Pool)

生产环境中往往需要对并发请求做限制,避免同时发起成百上千的请求压垮服务端或浏览器:

6.2 自动重试机制

网络请求可能因超时、网络抖动等原因失败,自动重试是常用策略:

6.3 超时包装

给异步操作添加超时控制,超时后自动拒绝:

6.4 竞态条件处理

多个异步请求中只关心"最新"的那个,放弃旧请求的结果:

6.5 取消 fetch(AbortController)

通过 AbortController 可以主动取消正在进行的 fetch 请求:


总结

JavaScript 异步编程的演进可以概括为三个阶段:

阶段 核心机制 优点 缺点
回调 传递函数作为参数 简单、原生支持 回调地狱、错误处理困难
Promise 状态机 + 链式调用 扁平化、错误穿透、组合能力强 语法仍不够直观
async/await 语法糖 + 生成器 同步写法、try/catch 自然、可读性强 需理解底层 Promise

但无论上层语法如何变化,事件循环始终是所有异步行为的底层基础。理解调用栈、宏任务、微任务三者的协作关系,才能准确预测任何异步代码的执行顺序。

实践中建议遵循以下原则:

  1. 优先使用 async/await,代码可读性最好
  2. 无依赖的任务并行执行(Promise.all)
  3. 并发操作做好限流(Promise Pool)
  4. 重要操作实现自动重试(带指数退避)
  5. 涉及用户交互时处理竞态(搜索场景)
  6. 长耗时操作提供超时和取消支持(AbortController)

掌握这些模式后,无论是浏览器中的 UI 交互,还是 Node.js 中的 I/O 密集型服务,你都能编写出高效、健壮的异步代码。


参考资源