JavaScript 异步与 Promise

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Node.js 22 LTS

单线程却要等网络、等文件、等定时器,所以 JavaScript 把「等」做成异步。理解 Promise 和 async/await 之后,异步代码的顺序就不再靠猜。

环境准备

示例在 Node.js 22 LTS 下验证;浏览器里同样的写法可以直接用(fetch、setTimeout 都是全局的)。

事件循环:谁先执行

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

console.log("4");

输出是 1 4 3 2:

  • 同步代码先跑完(1、4)。
  • 微任务队列(Promise 回调)先清空(3)。
  • 然后才是宏任务(定时器,2)。

记住这点就够了:Promise 回调总比 setTimeout(…, 0) 早。每个 await 之后恢复的部分相当于放进微任务队列,所以循环里 await 会交出执行权。

Promise 的三种状态

const p = fetch("/api/items");
  • pending:还没有结果。
  • fulfilled:成功,带一个值。
  • rejected:失败,带一个原因。

状态一旦改变就不会再变,所以重复 resolve 无效——这解释了为什么「同一个 Promise 多次 then 拿到的是同一个结果」。

链式调用

fetch("/api/user")
  .then((res) => res.json())
  .then((user) => user.name)
  .then((name) => console.log(name))
  .catch((err) => console.error("失败:", err))
  .finally(() => console.log("结束"));

要点:

  • then 返回新 Promise,返回普通值会被自动包成 Promise.resolve(value)。
  • 在 then 里 return 一个 Promise,链会等它完成再继续(不会得到嵌套 Promise)。
  • catch 捕获它之前任何一环的错误;finally 不改变结果,只做清理。

async / await

async 函数总是返回 Promise;await 把 Promise 展开成值,等价于在 then 里继续写。

async function loadUserName(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const user = await res.json();
  return user.name;
}

错误处理两种写法,别混着乱用:

// 1) try/catch:需要处理或补充上下文时
try {
  const name = await loadUserName(7);
} catch (err) {
  console.error("加载失败", err);
}

// 2) 直接往外抛:让调用方的 catch 统一处理
async function handler() {
  const name = await loadUserName(7);
  return name;
}

不要写出「await 包在 try 里但 catch 里啥也不做」的代码,那会把失败吞掉。

串行与并行

// 串行:总耗时 ≈ 三个请求之和
const a = await getUser(1);
const b = await getOrders(1);
const c = await getStats(1);

// 并行:总耗时 ≈ 最慢的那个
const [a2, b2, c2] = await Promise.all([getUser(1), getOrders(1), getStats(1)]);

互相不依赖的请求就该并行。串行只在「后一个要用前一个的结果」时才必要。

组合器

// all:全部成功才成功;任一失败立刻失败(其他请求不会取消)
const [user, orders] = await Promise.all([getUser(1), getOrders(1)]);

// allSettled:等全部有结果,成功失败都收,适合批量任务
const results = await Promise.allSettled(urls.map((u) => fetch(u)));
const failed = results.filter((r) => r.status === "rejected");

// race:第一个有结果(成功或失败)就采用,常用于超时
const withTimeout = Promise.race([
  fetch("/api/slow"),
  new Promise((_, reject) => setTimeout(() => reject(new Error("超时")), 3000)),
]);

// any:第一个成功就采用;全失败才 reject(AggregateError)
const fastest = await Promise.any([mirrorA(), mirrorB()]);

Promise.all 失败时不会取消其余请求——网络请求已经发出去了。要真正取消,见下一篇的 AbortController。

并发上限

一次发 500 个请求会打爆连接池,也可能被对方限流。控制并发数:

async function mapLimit(items, limit, worker) {
  const results = new Array(items.length);
  let cursor = 0;

  async function run() {
    while (cursor < items.length) {
      const index = cursor++;
      results[index] = await worker(items[index], index);
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
  return results;
}

await mapLimit(urls, 5, (url) => fetch(url));

启动 limit 个 worker,每个取下一个任务,天然限流。

把回调式 API 包成 Promise

老库仍是回调风格:

import { readFile } from "node:fs";

function readText(path) {
  return new Promise((resolve, reject) => {
    readFile(path, "utf8", (err, data) => (err ? reject(err) : resolve(data)));
  });
}

Node 现在提供 node:util 的 promisify,标准库也大多有 fs/promises 版本,优先用现成的。

常见错误与注意事项

1. 在 forEach 里用 async

// 不会等待,错误也没人接
items.forEach(async (item) => {
  await save(item);
});

// 正确:并行
await Promise.all(items.map((item) => save(item)));

// 正确:串行
for (const item of items) {
  await save(item);
}

2. 忘了 await

save(data);        // 返回的 Promise 没人管,失败会变成 unhandledRejection
await save(data);  // 才是等它完成

3. 在 try 外漏掉 await

try { save(data) } catch {} 捕不到异步错误,因为异常发生在 Promise 里。必须 await。

4. await 写在循环里但本可以并行

// 慢
const users = [];
for (const id of ids) users.push(await getUser(id));

// 快
const users = await Promise.all(ids.map(getUser));

5. 未处理的 rejection

Node 会打印警告并在未来版本终止进程。给顶层入口加兜底:

process.on("unhandledRejection", (err) => {
  console.error("未处理的 Promise 失败", err);
  process.exitCode = 1;
});

6. async 函数里 return await 的取舍

在 try/catch 外,return await promise 和 return promise 结果相同,但前者多一次微任务;在 try 内则必须 await 才能被 catch 捕获。

小结

  • 同步 → 微任务(Promise)→ 宏任务(定时器),顺序固定。
  • Promise 状态不可逆;then 返回新 Promise,catch 捕获链上之前的错误。
  • async/await 是语法糖,错误处理靠 try/catch 或向上抛。
  • 无依赖的异步任务用 Promise.all 并行;需要全部结果用 allSettled;超时用 race。
  • 批量任务要限并发;forEach 配 async 是最常见的坑。

下一步