JavaScript 异步与 Promise
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
单线程却要等网络、等文件、等定时器,所以 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是最常见的坑。