面试官:说说async/await?我差点翻车!原来还可以这么用

"你可以说说,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"

你可以说:

  1. "它是 Promise 的语法糖,让异步代码更易读。
  2. 但它本质还是异步,await会把后续逻辑注册为 .then 回调,进入微任务队列。
  3. 使用时要注意并行优化、错误捕获,避免在数组方法中误用。

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot 中的 7 种耗时统计方式,你用过几种?》

《MySQL 为什么不推荐用雪花ID 和 UUID 做主键?》

《Vue3 如何优雅地实现一个全局的 loading 组件》

《Vue3+CSS实现一个非常丝滑的 input 标签上浮动画》

相关推荐
90后的晨仔5 分钟前
在macOS上无缝整合:为Claude Code配置魔搭社区免费API完全指南
前端
沿着路走到底35 分钟前
JS事件循环
java·前端·javascript
子春一21 小时前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶1 小时前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied3 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一23 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记