Skip to main content

JS await 基础入门类

· 4 min read

JS 中 async/await 不是新功能,是 Promise 的语法糖——本质还是回调。

掌握 await 只需记住 件事:

  1. await 只能用在 async 函数里,async 函数永远返回 Promise
  2. await 会"暂停"当前函数,但不会阻塞主线程,其他代码照常跑
  3. 抛错用 try/catch,忘记 try/catch 会导致 UnhandledPromiseRejection

最佳实践:并发请求用 Promise.all,别串行 await,慢到怀疑人生。


一句话讲清 await 是什么

await 就是"等这个 Promise 出结果,再往下走"。

写回调时代我们这么写:

fetchUser(id, (user) => {
fetchOrders(user.id, (orders) => {
render(orders);
});
});

Promise 时代变成链式:

fetchUser(id)
.then((user) => fetchOrders(user.id))
.then((orders) => render(orders));

async/await 时代,写起来像同步代码:

async function load(id) {
const user = await fetchUser(id);
const orders = await fetchOrders(user.id);
render(orders);
}

代码长得像同步,但执行起来还是异步的——这是 await 最容易让人误解的地方。

async 函数的真面目

任何 async 函数都返回 Promise,不管你写没写 return。

async function foo() {
return 1;
}

foo(); // Promise { 1 }
foo().then(console.log); // 1

就算函数里 throw new Error('boom'),外面拿到的也是一个 rejected 的 Promise,不是同步抛出的异常。这意味着你不能用普通 try/catch 包住 foo() 调用本身,得包住 await foo()

await 暂停的是函数,不是线程

这是初学者最容易踩的坑。看这段代码:

async function task() {
console.log('A');
await sleep(1000);
console.log('B');
}

task();
console.log('C');

输出顺序是 A → C → B

await 暂停的是 task 函数的执行,主线程没停,所以 C 先打印。等 1 秒后 task 才被恢复,打印 B

**JS 是单线程的,await 从来不会"卡住"页面。**如果你的页面卡了,那一定是同步代码在算东西,不是 await 的锅。

错误处理:try/catch 是底线

Promise 出错不抛异常,而是把 Promise 变成 rejected 状态。await 一个 rejected Promise,才会在 await 的位置真的抛错:

async function load() {
try {
const data = await fetch('/api/x').then((r) => r.json());
return data;
} catch (err) {
console.error('请求失败:', err);
return null;
}
}

没有 try/catch 又没有 .catch() 的 Promise rejection,在 Node 里会触发 UnhandledPromiseRejection,新版本直接让进程崩溃。浏览器里则会在 Console 报红。

串行还是并行?一个数量级的差距

新手最常见的反模式:

const a = await fetchA(); // 等 1s
const b = await fetchB(); // 再等 1s
const c = await fetchC(); // 再等 1s
// 总共 3s

三个请求互不依赖,却被强行串行,总耗时 3 秒。

正确写法是 Promise.all:

const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// 总共 1s

判断标准很简单:后一个请求需不需要前一个的结果?

  • 需要 → 串行 await
  • 不需要 → Promise.all 并发

如果其中一个失败不影响其他,用 Promise.allSettled,它不会因为单个 rejected 就整体失败。

循环里的 await:别用 forEach

Array.prototype.forEach 不识别 async 回调,await 完全无效:

[1, 2, 3].forEach(async (id) => {
await save(id); // 不会按顺序执行
});
console.log('done'); // 立刻打印,根本没等

要顺序执行用 for...of:

for (const id of ids) {
await save(id);
}

要并发执行用 map + Promise.all:

await Promise.all(ids.map((id) => save(id)));

最后一点:顶层 await

ES2022 之后,ESM 模块顶层可以直接用 await,不用再包一层 async function main():

// index.mjs
const config = await fetch('/config.json').then((r) => r.json());
export default config;

但只在 .mjspackage.json 配了 "type": "module" 的项目里能用,CommonJS 的 .js 文件还是老老实实包 async 函数吧。