"你可以说说,async/await 到底是个啥吗?"
这是我以前在一场前端面试里被问到的一个问题。
当时我脑子一懵,差点脱口而出:"就是让异步代码看起来像同步嘛......"
话到嘴边,我意识到:这回答太轻飘了。
面试官要的,不是背定义。
他要的是:你懂它的本质,知道它解决了什么问题,更清楚它的边界和陷阱。
今天这篇文章我们聊聊async/await
。
一、前言
async/await
是什么?为什么会有它?
我们先说结论:
async/await
是 JavaScript 中处理异步操作的一种语法糖。
它基于Promise
,但让异步代码写起来更像同步,读起来更清晰。
那问题来了:
为啥需要它?Promise 不香吗?
1. 为什么会有 async/await?
因为------回调地狱(Callback Hell)太难受了。
早年写异步,都是回调:
javascript
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(result) {
console.log(result);
});
});
});
});
层层嵌套,缩进比代码还长。
一眼望去,全是花括号和 function
。
后来ES6
推出了 Promise,链式调用,清爽多了:
javascript
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => getFinalData(c))
.then(result => console.log(result))
.catch(err => console.error(err));
看着是好了。
但实际开发中,你会发现:
- 条件判断麻烦(比如:只有满足某个条件才继续下一步)
- 中间变量传递绕(得靠
then
一层层传) try/catch
捕获不了 Promise 内部错误(得用.catch()
)- 循环处理异步?写起来非常绕
于是,async/await 出现了。
它让我们可以用同步的写法,写异步逻辑。
看起来像"阻塞",实则不阻塞。
关键是:可读性可以大大的提升。
二、async/await 怎么用?
1. 基本语法
javascript
async function fetchData() {
try {
const response = await fetch('/api/user');
const user = await response.json();
console.log(user);
} catch (error) {
console.error('出错了:', error);
}
}
对,就这么简单,但你得明白三点:
1. async
函数一定会返回一个 Promise
javascript
async function hello() {
return 'world';
}
hello(); // 返回的是 Promise<string>
所以你可以这么接:
javascript
hello().then(console.log); // 'world'
补充:
- 如果
async
函数return
一个值,会自动包装成Promise.resolve(value)
- 如果
throw
错误,会变成Promise.reject(error)
javascript
async function errorFunc() {
throw new Error('boom!');
}
errorFunc().catch(err => console.log(err.message)); // 'boom!'
2. await
只能在async
函数里用
javascript
function bad() {
await fetchData(); // SyntaxError
}
await
必须在async
函数内部。否则,直接报错。
3. await
等的是一个 Promise
javascript
const result = await somePromise();
如果等的不是 Promise?
那也没事,JavaScript会自动把它包装成Promise.resolve()
。
javascript
const num = await 42; // 等同于 await Promise.resolve(42)
console.log(num); // 42
三、async/await 到底做了啥?
async/await
的设计思想类很似 Generator + co,但并不是基于 Generator 实现的。
它是 V8 引擎原生支持的特性,性能更好,机制更直接。
你可以把它理解成: await
把后续代码注册成Promise的.then
回调,放入微任务队列。
await promise
等价于:把
await
后面的代码,用.then
包起来,交给 Promise 处理。
举个例子:
javascript
console.log('1');
async function foo() {
console.log('2');
await Promise.resolve();
console.log('3');
}
foo();
console.log('4');
输出结果:
1
2
4
3
await Promise.resolve()
会把console.log('3')
放进微任务队列。
当前同步代码console.log('4')
执行完后,事件循环才处理微任务。
这就是 await
的真相:
它不是真的暂停,而是把后续逻辑放进微任务,等当前同步代码执行完再执行。
四、async/await 怎么用才对?
光会写 await
不够,关键是怎么用得好。
场景 1:串行 vs 并行
你有三个接口,要等全部返回。
错误写法:串行等待
javascript
async function bad() {
const a = await fetchA(); // 等 200ms
const b = await fetchB(); // 再等 200ms
const c = await fetchC(); // 再等 200ms
// 总耗时 ≈ 600ms
}
一个接一个,慢得像蜗牛。
正确写法:并行发起
javascript
async function good() {
const [a, b, c] = await Promise.all([
fetchA(),
fetchB(),
fetchC()
]);
// 总耗时 ≈ 200ms
}
Promise.all
同时发起三个请求,谁也不等谁。
等全部 resolve,再一起返回。
⚠️ 注意:Promise.all
是"全成功才成功",任何一个 reject,整个就reject。
如果你希望"失败也不影响",用
Promise.allSettled
。
javascript
const results = await Promise.allSettled([
fetchA(),
fetchB(),
fetchC()
]);
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.log('失败:', result.reason);
}
});
场景 2:条件判断 + 异步
比如:用户登录后,先查权限,再决定加载哪个页面。
javascript
async function loadPage() {
const user = await fetchUser();
if (user.isAdmin) {
const data = await fetchAdminData();
renderAdminPage(data);
} else {
const data = await fetchUserData();
renderUserPage(data);
}
}
这种逻辑,用Promise链写,得嵌套 .then
里的 .then
。
用async/await
,会非常的清晰。
场景 3:循环中使用await
常见错误:
javascript
async function badLoop() {
const ids = [1, 2, 3, 4, 5];
for (let id of ids) {
await fetchUser(id); // 一个一个等,串行!
}
}
如果每个请求 100ms,5 个就是 500ms。
改法 1:并行发起,等全部完成
javascript
async function goodLoop1() {
const ids = [1, 2, 3, 4, 5];
await Promise.all(ids.map(id => fetchUser(id)));
}
改法 2:需要顺序处理?用for...of
javascript
async function goodLoop2() {
const ids = [1, 2, 3, 4, 5];
for (let id of ids) {
// 必须等上一个完成再进行下一个
const user = await fetchUser(id);
process(user);
}
}
关键看需求:
要快?用 Promise.all
。
要顺序?用 for...of
+ await
。
五、async/await的坑,你踩过几个?
坑 1:忘记 try/catch
javascript
async function forgotCatch() {
const res = await fetch('/api/data'); // 如果网络出错?
return res.json();
}
如果fetch
失败,这个函数会抛出异常。
但没人接,就变成 未捕获的 Promise rejection。
浏览器:Uncaught (in promise) TypeError: Failed to fetch
解决:加 try/catch
javascript
async function safeFetch() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (err) {
console.error('请求失败:', err);
return null;
}
}
或者,你也可以在外面.catch()
:
javascript
safeFetch().catch(err => console.log(err));
坑 2:在 forEach/map 中用 await,无效!
javascript
const urls = ['/a', '/b', '/c'];
urls.map(async (url) => {
const res = await fetch(url);
console.log(await res.text());
});
console.log('done'); // 这行会先打印!
为什么?
因为map
的回调是async
函数,返回的是 Promise。
但map
本身不会await
这些 Promise。
所以这些请求是并发的,但主流程不等它们。
解决:用 for...of
javascript
for (let url of urls) {
const res = await fetch(url);
console.log(await res.text());
}
console.log('done'); // 这次等完了才打印
或者用 Promise.all
包一层:
javascript
await Promise.all(urls.map(async (url) => {
const res = await fetch(url);
console.log(await res.text());
}));
六、高级用法和技巧
1. 异步迭代器
处理数据流时很有用:
js
async function processStream(stream) {
for await (const chunk of stream) {
await processChunk(chunk);
}
}
2. 重试机制
实现自动重试:
js
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
if (i === retries - 1) throw error;
await sleep(1000 * (i + 1)); // 重试间隔逐渐增加
}
}
}
3. 超时控制
js
async function fetchWithTimeout(url, timeout = 5000) {
const fetchPromise = fetch(url);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), timeout);
});
return await Promise.race([fetchPromise, timeoutPromise]);
}
七、总结
async/await
只是让异步代码更好写、更好读。但它还是解决不了异步本身的复杂性。
所以,下次面试官问你:"讲讲 async/await"
你可以说:
- "它是 Promise 的语法糖,让异步代码更易读。
- 但它本质还是异步,
await
会把后续逻辑注册为.then
回调,进入微任务队列。 - 使用时要注意并行优化、错误捕获,避免在数组方法中误用。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《SpringBoot 中的 7 种耗时统计方式,你用过几种?》
《MySQL 为什么不推荐用雪花ID 和 UUID 做主键?》