摘要

本文深入解析 async/await 的底层原理、语法规范、异常处理技巧,结合大量真实业务场景,拆解 async/await 与 Promise、Generator 的关联,提供优雅的异常捕获方案,对比 Promise 链式调用的优劣,覆盖面试高频考点(原理、异常处理、串行 / 并行实践),帮助开发者摆脱回调嵌套与链式调用的繁琐,写出更简洁、易维护的异步代码。


一、前言:async/await 为什么能替代 Promise 链式调用?

Promise 的出现解决了回调地狱,但当异步操作较多时,链式调用(then 链)依然会显得繁琐,代码可读性、可维护性不足,且异常处理需要层层 catch,不够优雅。

Promise 链式调用的痛点示例

javascript

运行

// 串行请求:获取用户 → 获取订单 → 获取详情
getRequest("/api/user")
  .then((user) => getRequest(`/api/order?userId=${user.id}`))
  .then((order) => getRequest(`/api/detail?orderId=${order.id}`))
  .then((detail) => {
    console.log(detail);
    // 后续逻辑...
  })
  .catch((err) => {
    console.log("请求错误:", err);
  });

虽然比回调地狱简洁,但依然需要不断嵌套 then,逻辑连贯性差,异常捕获只能统一处理(无法精准捕获单个请求错误)。

async/await 的优势

async/await 是 Promise + Generator 函数的语法糖,它的核心作用是:用同步代码的写法,实现异步操作,彻底摆脱 then 链,代码更简洁、逻辑更清晰,异常处理更灵活。

改造后(async/await 版本):

javascript

运行

// 同步写法,实现异步逻辑
async function getDetail() {
  try {
    const user = await getRequest("/api/user");
    const order = await getRequest(`/api/order?userId=${user.id}`);
    const detail = await getRequest(`/api/detail?orderId=${order.id}`);
    console.log(detail);
  } catch (err) {
    console.log("请求错误:", err);
  }
}

代码完全是同步的结构,却能执行异步操作,可读性大幅提升,异常处理也更灵活。


二、async/await 基础语法(必背)

1. 核心定义

  • async:用于修饰函数,标记该函数是一个异步函数,函数执行后永远返回一个 Promise(无论函数内部返回什么值,都会被转为 Promise.resolve(值))。
  • await:只能用在 async 函数内部,用于等待一个 Promise 完成(fulfilled 或 rejected),暂停函数执行,直到 Promise 有结果后,再继续执行后续代码。

2. 基础用法(3 个核心示例)

示例 1:基本使用(成功场景)

javascript

运行

// async 修饰函数,返回 Promise
async function fn() {
  // await 等待 Promise 成功,获取结果
  const res = await Promise.resolve("异步操作成功");
  console.log(res); // 异步操作成功
  return res; // 等同于 return Promise.resolve(res)
}

// 调用 async 函数
fn().then((res) => console.log("函数返回结果:", res)); // 函数返回结果:异步操作成功
示例 2:await 等待失败的 Promise

javascript

运行

async function fn() {
  try {
    // await 等待 Promise 失败,会抛出错误,需用 try/catch 捕获
    const res = await Promise.reject(new Error("异步操作失败"));
    console.log(res); // 不会执行(因为前面抛出错误)
  } catch (err) {
    console.log("捕获错误:", err.message); // 捕获错误:异步操作失败
  }
}

fn();
示例 3:async 函数返回值自动转为 Promise

javascript

运行

// 1. 返回普通值 → 转为 Promise.resolve(普通值)
async function fn1() {
  return 100;
}
fn1().then((res) => console.log(res)); // 100

// 2. 返回 Promise → 直接返回该 Promise
async function fn2() {
  return Promise.resolve(200);
}
fn2().then((res) => console.log(res)); // 200

// 3. 抛出错误 → 转为 Promise.reject(错误)
async function fn3() {
  throw new Error("主动抛出错误");
}
fn3().catch((err) => console.log(err.message)); // 主动抛出错误

3. 关键注意点(面试避坑)

  1. await 只能用在 async 函数内部,普通函数中使用会报错(语法错误)。
  2. async 函数执行时,遇到 await 会暂停执行(不会阻塞整个 JS 线程,只是暂停当前函数),直到 Promise 完成。
  3. await 后面可以跟任意值:如果是 Promise,等待其结果;如果是非 Promise,直接返回该值(等同于 await Promise.resolve(值))。
  4. async 函数的返回值,永远是 Promise(即使没有 return 语句,也会返回 Promise.resolve(undefined))。

三、async/await 底层原理(面试必考)

核心结论

async/await 本质是 Generator 函数 + Promise 的语法糖,浏览器 / Node.js 内部会自动执行 Generator 函数,无需手动调用 next () 方法,简化了 Generator 函数的使用。

1. 先了解 Generator 函数(基础铺垫)

Generator 函数是 ES6 推出的异步编程方案,核心是 “可暂停、可恢复”,用 function* 定义,用 yield 暂停执行,用 next() 恢复执行。

示例(Generator 实现异步):

javascript

运行

// Generator 函数(带 *)
function* gen() {
  yield Promise.resolve(1); // 暂停,返回 Promise
  yield Promise.resolve(2); // 暂停,返回 Promise
  return 3;
}

// 手动执行 Generator 函数
const g = gen();
g.next().value.then((res) => {
  console.log(res); // 1
  return g.next().value;
}).then((res) => {
  console.log(res); // 2
  return g.next().value;
}).then((res) => {
  console.log(res); // 3
});

可以看到,Generator 函数需要手动调用 next (),且需要结合 then 链处理异步,使用繁琐 —— 这就是 async/await 要解决的问题。

2. async/await 与 Generator 的关联

async 函数本质上是对 Generator 函数的 “自动执行封装”,相当于:

  1. 用 async 替代 function*
  2. 用 await 替代 yield
  3. 自动执行 Generator 函数的 next () 方法,无需手动调用
  4. 自动处理 Promise 结果,将结果返回给 await,无需手动 then

3. 手动模拟 async/await(面试加分)

用 Promise 自动执行 Generator 函数,模拟 async/await 的底层逻辑:

javascript

运行

// 模拟 async/await 的自动执行器
function myAsync(genFn) {
  // 返回一个 Promise(对应 async 函数的返回值)
  return new Promise((resolve, reject) => {
    const gen = genFn(); // 执行 Generator 函数,得到迭代器

    // 自动执行 next()
    function next(res) {
      const result = gen.next(res); // 执行 next,获取 { value, done }
      if (result.done) {
        //  Generator 执行完毕,resolve 最终结果
        return resolve(result.value);
      }
      // 若 value 是 Promise,等待其完成后,继续执行 next
      Promise.resolve(result.value).then(
        (val) => next(val), // 成功:将结果传给下一个 yield
        (err) => reject(err) // 失败:直接 reject
      );
    }

    // 启动自动执行
    next();
  });
}

// 测试模拟的 async/await
function* gen() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(a + 1);
  return b + 1;
}

// 用 myAsync 模拟 async/await 调用
myAsync(gen).then((res) => console.log(res)); // 3

通过这个模拟,能清晰看到:async/await 就是帮我们自动执行 Generator 函数,自动处理 Promise 结果,简化了手动调用的繁琐操作。

面试必答:async/await 与 Promise 的关系

  1. async/await 是基于 Promise 实现的,不能脱离 Promise 单独使用(await 后面必须是 Promise 或可转为 Promise 的值)。
  2. async 函数的返回值是 Promise,因此可以继续用 then/catch 处理结果。
  3. async/await 是 Promise 的 “语法糖”,没有新增异步机制,只是简化了 Promise 的使用方式。

四、async/await 异常处理(实战重点)

async/await 的异常处理,核心是 try/catch 语句,比 Promise 的 catch 更灵活,可以精准捕获单个异步操作的错误,也可以统一捕获所有错误。

1. 统一捕获所有错误(简单场景)

用 try 包裹所有 await 操作,catch 捕获所有异步错误(包括单个 await 的失败):

javascript

运行

async function fetchData() {
  try {
    // 所有 await 操作都在 try 中
    const user = await getRequest("/api/user");
    const order = await getRequest(`/api/order?userId=${user.id}`);
    const detail = await getRequest(`/api/detail?orderId=${order.id}`);
    console.log(detail);
  } catch (err) {
    // 统一捕获所有错误(任何一个 await 失败,都会进入 catch)
    console.log("捕获错误:", err.message);
    // 可做错误处理(如提示用户、重试请求)
  }
}

2. 精准捕获单个错误(复杂场景)

如果需要对不同的异步操作,做不同的错误处理,可给每个 await 单独添加 try/catch:

javascript

运行

async function fetchData() {
  let user = null;
  // 捕获获取用户的错误
  try {
    user = await getRequest("/api/user");
  } catch (err) {
    console.log("获取用户失败:", err.message);
    return; // 失败后,终止后续操作
  }

  let order = null;
  // 捕获获取订单的错误
  try {
    order = await getRequest(`/api/order?userId=${user.id}`);
  } catch (err) {
    console.log("获取订单失败:", err.message);
    // 可做重试逻辑
    order = await getRequest(`/api/order?userId=${user.id}`);
  }

  // 捕获获取详情的错误
  try {
    const detail = await getRequest(`/api/detail?orderId=${order.id}`);
    console.log(detail);
  } catch (err) {
    console.log("获取详情失败:", err.message);
  }
}

3. 结合 Promise.catch 处理错误(补充方案)

await 后面的 Promise 可以直接调用 catch,捕获单个异步操作的错误,不影响后续代码执行:

javascript

运行

async function fetchData() {
  // 单个 await 错误捕获(不影响后续 await 执行)
  const user = await getRequest("/api/user").catch((err) => {
    console.log("获取用户失败:", err.message);
    return null; // 失败后返回默认值,避免后续代码报错
  });

  if (!user) return; // 若获取用户失败,终止后续操作

  const order = await getRequest(`/api/order?userId=${user.id}`).catch((err) => {
    console.log("获取订单失败:", err.message);
    return null;
  });

  if (!order) return;

  const detail = await getRequest(`/api/detail?orderId=${order.id}`);
  console.log(detail);
}

4. 面试避坑:常见异常处理误区

  • 误区 1:忘记用 try/catch,await 失败会导致整个 async 函数返回 rejected Promise,且错误会 “冒泡”,若未捕获,会触发 Uncaught Error。
  • 误区 2:多个 await 并行执行时,用 try/catch 统一捕获,无法区分哪个 await 失败(需单独捕获或结合 Promise.all 处理)。
  • 误区 3:认为 await 会阻塞整个 JS 线程 —— 其实不会,await 只暂停当前 async 函数,JS 线程会继续执行其他同步代码。

五、async/await 实战场景(真实项目常用)

场景 1:异步串行执行(按顺序执行多个异步操作)

最常用场景:多个异步操作有依赖关系(如先获取用户,再根据用户 ID 获取订单)

javascript

运行

// 串行请求:用户 → 订单 → 详情
async function getOrderDetail() {
  try {
    // 1. 获取用户
    const { data: user } = await axios.get("/api/user");
    // 2. 根据用户ID获取订单(依赖用户数据)
    const { data: order } = await axios.get(`/api/order?userId=${user.id}`);
    // 3. 根据订单ID获取详情(依赖订单数据)
    const { data: detail } = await axios.get(`/api/detail?orderId=${order.id}`);
    
    // 渲染数据
    renderDetail(detail);
    return detail;
  } catch (err) {
    console.log("请求失败:", err);
    ElMessage.error("获取数据失败,请重试"); // 结合UI组件提示
  }
}

场景 2:异步并行执行(同时执行多个异步操作,提高效率)

场景:多个异步操作无依赖关系(如同时获取首页多个模块的数据),用 Promise.all 结合 async/await 实现并行:

javascript

运行

// 并行请求:同时获取首页轮播、推荐、公告数据
async function getHomeData() {
  try {
    // 用 Promise.all 并行执行多个请求
    const [bannerRes, recommendRes, noticeRes] = await Promise.all([
      axios.get("/api/banner"),
      axios.get("/api/recommend"),
      axios.get("/api/notice"),
    ]);

    // 分别处理数据
    const banner = bannerRes.data;
    const recommend = recommendRes.data;
    const notice = noticeRes.data;

    // 渲染首页
    renderHome(banner, recommend, notice);
  } catch (err) {
    console.log("首页数据请求失败:", err);
  }
}

⚠️ 注意:并行执行时,只要有一个请求失败,Promise.all 会立即失败,进入 catch;若需允许部分失败,可用 Promise.allSettled。

场景 3:异步并行 + 部分容错(允许部分请求失败)

javascript

运行

async function getHomeData() {
  // 用 allSettled 并行执行,获取所有结果
  const results = await Promise.allSettled([
    axios.get("/api/banner"),
    axios.get("/api/recommend"),
    axios.get("/api/notice"),
  ]);

  // 筛选成功的结果,处理失败的结果
  const banner = results[0].status === "fulfilled" ? results[0].value.data : [];
  const recommend = results[1].status === "fulfilled" ? results[1].value.data : [];
  const notice = results[2].status === "fulfilled" ? results[2].value.data : [];

  // 即使部分失败,也渲染首页(用默认值替代失败数据)
  renderHome(banner, recommend, notice);

  // 记录失败的请求
  const failRequests = results.filter(item => item.status === "rejected");
  if (failRequests.length > 0) {
    console.log("部分请求失败:", failRequests);
  }
}

场景 4:异步重试机制(失败后自动重试)

javascript

运行

// 封装带重试的请求函数
async function requestWithRetry(url, retryCount = 3, delay = 1000) {
  try {
    const { data } = await axios.get(url);
    return data;
  } catch (err) {
    // 重试次数大于0,继续重试
    if (retryCount > 0) {
      console.log(`请求失败,剩余重试次数:${retryCount},${delay}ms后重试`);
      // 延迟重试(用 setTimeout 包裹,返回 Promise)
      await new Promise(resolve => setTimeout(resolve, delay));
      return requestWithRetry(url, retryCount - 1, delay); // 递归重试
    }
    // 重试次数耗尽,抛出错误
    throw new Error(`请求失败,已重试${3}次:${err.message}`);
  }
}

// 使用
async function fetchData() {
  try {
    const data = await requestWithRetry("/api/data", 3, 1000);
    console.log(data);
  } catch (err) {
    console.log(err.message);
  }
}

六、async/await vs Promise 链式调用(面试必比)

表格

对比项 async/await Promise 链式调用
代码结构 同步写法,逻辑连贯 链式嵌套,可读性一般
异常处理 try/catch 灵活捕获(单个 / 统一) 只能链式 catch,精准捕获繁琐
调试体验 可直接断点调试,步骤清晰 断点调试需跟进 then 链,不够直观
学习成本 低(同步思维,易理解) 中(需理解链式调用和状态流转)
适用场景 复杂异步逻辑、多依赖异步操作 简单异步操作、少量链式调用

总结建议

  • 简单异步操作(如单个接口请求):用 Promise 或 async/await 均可,看个人习惯。
  • 复杂异步逻辑(多依赖、多步骤、需精准异常处理):优先用 async/await,代码更简洁、易维护、易调试。
  • 并行异步操作:结合 Promise.all/allSettled + async/await,兼顾效率和可读性。

七、高频面试题(必背标准答案)

  1. async/await 是什么?底层原理是什么?答:async/await 是 Promise + Generator 函数的语法糖,用于用同步代码的写法实现异步操作;底层是通过自动执行 Generator 函数,结合 Promise 处理异步结果,简化了 Generator 函数的手动调用。

  2. async 函数的返回值是什么?答:async 函数的返回值永远是一个 Promise;如果函数内部返回普通值,会转为 Promise.resolve (普通值);如果抛出错误,会转为 Promise.reject (错误)。

  3. await 只能用在什么地方?如果 await 后面跟的不是 Promise,会怎么样?答:await 只能用在 async 函数内部;如果 await 后面跟的不是 Promise,会直接返回该值,等同于 await Promise.resolve (值)。

  4. async/await 如何处理异常?有几种方式?答:主要用 try/catch 处理异常,有两种方式:① 统一捕获(try 包裹所有 await,catch 统一处理);② 精准捕获(每个 await 单独 try/catch,或结合 Promise.catch)。

  5. async/await 和 Promise 链式调用的区别?各自的优缺点?答:async/await 是同步写法,逻辑连贯、异常处理灵活、调试方便,但依赖 async 函数;Promise 链式调用是异步写法,可读性一般、异常捕获繁琐,但无需依赖 async 函数,兼容性更好(低版本浏览器需兼容)。

  6. async/await 会阻塞 JS 线程吗?为什么?答:不会;await 只会暂停当前 async 函数的执行,不会阻塞整个 JS 线程,JS 线程会继续执行其他同步代码,直到 await 等待的 Promise 完成,再恢复当前 async 函数的执行。


八、总结

  1. async/await 是 Promise + Generator 的语法糖,核心是 “同步写法实现异步操作”,彻底简化异步代码。
  2. async 修饰函数,返回 Promise;await 只能用在 async 内部,等待 Promise 完成并返回结果。
  3. 异常处理核心是 try/catch,可实现统一捕获或精准捕获,比 Promise 链式调用更灵活。
  4. 实战中,串行异步用 await 顺序执行,并行异步用 Promise.all + await,提高效率。
  5. 掌握其底层原理(Generator + Promise)和异常处理技巧,是前端面试的必备能力。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐