面试官:说说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 标签上浮动画》

相关推荐
菥菥爱嘻嘻3 小时前
输出---修改ant样式
前端·react.js·anti-design-vue
该用户已不存在4 小时前
这6个网站一旦知道就离不开了
前端·后端·github
Ai行者心易4 小时前
10天!前端用coze,后端用Trae IDE+Claude Code从0开始构建到平台上线
前端·后端
东东2334 小时前
前端开发中如何取消Promise操作
前端·javascript·promise
掘金安东尼4 小时前
官方:什么是 Vite+?
前端·javascript·vue.js
柒崽4 小时前
ios移动端浏览器,vh高度和页面实际高度不匹配的解决方案
前端
烛阴4 小时前
为什么游戏开发者都爱 Lua?零基础快速上手指南
前端·lua
大猫会长5 小时前
tailwindcss出现could not determine executable to run
前端·tailwindcss
Moonbit5 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员