今天,我们来深入探讨 async/await 语法以及如何处理复杂的异步链式调用。这是一个现代 JavaScript 中非常重要且强大的特性。


第一部分:async/await 语法精讲

async/await 是建立在 Promise 之上的语法糖,它让异步代码的书写和阅读更像同步代码,从而避免了“回调地狱”并简化了错误处理。

1. async 函数
  • 定义: 在一个函数声明前加上 async 关键字,这个函数就成为了一个异步函数。
  • 返回值async 函数永远返回一个 Promise
    • 如果函数内部返回了一个非 Promise 的值(例如 return 42;),async 函数会自动用 Promise.resolve() 将其包装成一个已解决的 Promise。
    • 如果函数内部返回了一个 Promise,那么 async 函数就直接返回这个 Promise。
    • 如果函数内部抛出一个异常(throw new Error(...)),async 函数会返回一个被拒绝的 Promise。
async function foo() {
  return 42;
}
// 等价于:
function foo() {
  return Promise.resolve(42);
}

foo().then(value => console.log(value)); // 42

async function bar() {
  throw new Error('Oops!');
}
// 等价于:
function bar() {
  return Promise.reject(new Error('Oops!'));
}

bar().catch(error => console.error(error)); // Error: Oops!
2. await 表达式
  • 作用await 关键字只能在 async 函数内部使用。它用于“暂停”异步函数的执行,等待一个 Promise 对象被解决(fulfilled 或 rejected),然后恢复函数的执行。
  • 行为
    • 如果 await 后面的表达式是一个 Promise,它会等待这个 Promise 完成。
    • 如果 Promise 被成功解决(fulfilled),await 表达式的结果就是 Promise 的解决值(resolution value)。
    • 如果 Promise 被拒绝(rejected),await抛出拒绝的原因(rejection reason),就像一个同步的 throw 语句一样。
    • 如果 await 后面的表达式不是 Promise,它会将其转换为一个已解决的 Promise(等同于 Promise.resolve(nonPromise)),然后返回这个值本身。
// 假设 fetchData 是一个返回 Promise 的函数
async function getData() {
  try {
    console.log('开始获取数据...');
    const result = await fetchData(); // “暂停”,等待 fetchData 的 Promise 完成
    console.log('数据获取成功:', result); // 上一行的 Promise 解决后,从这里继续执行
    return result;
  } catch (error) {
    console.error('获取数据失败:', error); // 如果 await 的 Promise 被拒绝,会跳到这里
  }
}

getData();
3. 错误处理

由于 await 会在 Promise 被拒绝时抛出异常,我们可以使用经典的 try...catch 结构来捕获错误,这使得异步代码的错误处理变得非常直观。

async function getUserProfile(userId) {
  try {
    const user = await fetchUser(userId); // 可能失败
    const posts = await fetchUserPosts(user.id); // 可能失败
    const avatar = await fetchUserAvatar(user.id); // 可能失败
    return { user, posts, avatar };
  } catch (error) {
    // 捕获任何一个 await 发生的错误
    console.error('加载用户资料失败:', error);
    // 可以返回一个默认值或重新抛出错误
    return { user: null, posts: [], avatar: null };
  }
}

你也可以选择不在 async 函数内部处理错误,而是让调用者使用 .catch() 来处理。

getUserProfile(123)
  .then(profile => console.log(profile))
  .catch(err => console.error('外层捕获错误:', err));

第二部分:复杂链式调用与优化

async/await 出现之前,复杂的异步依赖通常会导致很深的 Promise 链。现在,我们可以用更清晰的方式组织代码。

场景:有顺序依赖的链式调用

这是 async/await 最擅长的场景。代码从上到下顺序执行,可读性极佳。

// 旧的 Promise 链
function oldWay() {
  login(userCredentials)
    .then(user => getUserProjects(user.id))
    .then(projects => getProjectDetails(projects[0].id))
    .then(details => {
      console.log('第一个项目详情:', details);
    })
    .catch(error => {
      console.error('过程中出错:', error);
    });
}

// 使用 async/await (更清晰)
async function newWay() {
  try {
    const user = await login(userCredentials);
    const projects = await getUserProjects(user.id);
    const firstProjectDetails = await getProjectDetails(projects[0].id);
    console.log('第一个项目详情:', firstProjectDetails);
  } catch (error) {
    console.error('过程中出错:', error);
  }
}
场景:无顺序依赖的并行调用

如果多个异步操作之间没有依赖关系,使用 await 逐个等待会导致不必要的延迟。我们应该让它们同时发起。

// 低效写法:串行执行,总耗时 = time1 + time2 + time3
async function serialCalls() {
  const result1 = await fetchData1(); // 等待这个完成...
  const result2 = await fetchData2(); // ...再等待这个...
  const result3 = await fetchData3(); // ...最后等待这个
  return [result1, result2, result3];
}

// 高效写法:并行执行,总耗时 ≈ Max(time1, time2, time3)
async function parallelCalls() {
  // 立即启动所有 Promise,但不等待
  const promise1 = fetchData1();
  const promise2 = fetchData2();
  const promise3 = fetchData3();

  // 现在使用 await 等待所有 Promise 完成
  const result1 = await promise1;
  const result2 = await promise2;
  const result3 = await promise3;

  return [result1, result2, result3];
}

// 更简洁的写法:使用 Promise.all
async function parallelCallsWithPromiseAll() {
  // Promise.all 接收一个 Promise 数组,返回一个 Promise
  // 当所有输入的 Promise 都成功时,它返回一个结果数组
  // 如果有一个失败,整个 Promise.all 会立即拒绝
  const [result1, result2, result3] = await Promise.all([
    fetchData1(),
    fetchData2(),
    fetchData3(),
  ]);

  return [result1, result2, result3];
}
场景:混合复杂链式调用(实战)

假设一个复杂业务逻辑:

  1. 用户登录。
  2. 登录成功后,同时获取用户基本信息和权限列表。
  3. 根据权限列表中的第一个权限,去获取对应的详细数据。
  4. 将所有获取到的数据整合返回。
async function fetchComplexUserData(credentials) {
  try {
    // 1. 登录 (必须第一步)
    const authToken = await login(credentials);

    // 2. 并行获取用户信息和权限 (无依赖关系,可同时进行)
    const [userInfo, permissions] = await Promise.all([
      fetchUserInfo(authToken),
      fetchUserPermissions(authToken),
    ]);

    // 3. 根据权限列表的第一个权限ID,获取详情 (依赖上一步的 permissions)
    const primaryPermissionDetail = await fetchPermissionDetail(permissions[0].id);

    // 4. 整合所有数据
    return {
      user: userInfo,
      permissions: permissions,
      primaryPermission: primaryPermissionDetail,
    };

  } catch (error) {
    console.error('获取用户综合数据失败:', error);
    // 可以选择在这里处理特定错误,或者直接抛出
    throw error; // 让调用者知道失败
  }
}

// 调用
fetchComplexUserData({ username: 'foo', password: 'bar' })
  .then(data => console.log('成功:', data))
  .catch(err => console.error('最终失败:', err));

总结与最佳实践

  1. async/await 代替冗长的 .then(): 让代码更具可读性,尤其是对于有顺序依赖的异步操作。
  2. 善用 try...catch: 在 async 函数内部进行错误处理。
  3. 无依赖的操作使用并行: 使用 Promise.all() 来并行执行独立的异步任务,极大提升性能。
  4. 注意 await 的位置await 会阻塞其所在的 async 函数内的后续代码执行,但不会阻塞其他函数或全局代码。合理安排 await 的位置。
  5. 别忘了 async: 记住,只要函数体内包含了 await,这个函数就必须用 async 来声明。

通过结合 async/await 的清晰性和 Promise.all 的并行能力,你可以优雅且高效地处理任何复杂的异步编程场景。

Logo

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

更多推荐