跳到正文
前端知识库
JavaScript

14. 说说你对async/await的理解?

理解 async/await 的返回值与微任务恢复,按错误责任划分 try/catch 的作用域,并选择串行或并行执行。

约 6 分钟JavaScript · async · await · Promise · 错误处理 · 面试

14. 说说你对async/await的理解?

问题解析

async/await 是 ES2017 引入的异步编程语法,以 Promise 表达最终结果,让依赖前一步结果的异步代码按顺序书写。理解它的工作原理、执行顺序以及与 Promise 的关系是面试中的重要考点。

核心概念

async函数

async 关键字用于声明异步函数。每次调用都会返回一个新的 Promise:正常返回值用于解决它,未捕获的异常使它拒绝;返回 Promise/thenable 时会吸收其最终状态。

await关键字

await 可在 async 函数内部使用,也可在支持顶层 await 的 ES 模块顶层使用。它先求值右侧表达式,再等待该值;接受 Promise、thenable 和普通值。等待成功时取得结果,拒绝时在 await 处抛出原因。等待期间暂停当前函数或模块的后续执行,不会阻塞整个线程。

详细解答

async函数的基本用法

async function asyncFunction() {
  return 'Hello';
}

// 对这个普通返回值,结果可类比为
function promiseFunction() {
  return Promise.resolve('Hello');
}

asyncFunction().then(value => console.log(value)); // 'Hello'

这个类比不表示返回对象身份相同。例如 const p = Promise.resolve(1) 时,Promise.resolve(p) === p,但 async function pass() { return p; } 的 pass() !== p。原生 async 也不要求由 Generator 实现,详见 Generator 与 async/await 的关系。

await的基本用法

async function getData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('获取数据失败:', error);
  }
}

async/await的执行流程

async function fn1() {
  console.log(1);
  await fn2();
  console.log(2); // 被await阻塞,加入微任务队列
}

async function fn2() {
  console.log('fn2');
}

fn1();
console.log(3);

// 输出顺序: 1 -> fn2 -> 3 -> 2

执行过程分析:

  1. 执行fn1,打印1
  2. 遇到await fn2(),先执行fn2,打印fn2
  3. await阻塞后面的代码,将其加入微任务队列
  4. 跳出async函数,执行同步代码,打印3
  5. 同步代码执行完毕,执行微任务,打印2

深入理解

await会阻塞什么

await 会暂停 async 函数内部的后续代码,但不会阻塞主线程。待等待值敲定后,恢复执行通过微任务调度;即使等待的是普通值或已经兑现的 Promise,也会让出当前同步执行流程。await 右侧的求值仍是同步的,因此 await heavyCalculation() 不能让其中的 CPU 密集计算自动移到后台。

async function test() {
  console.log('开始');
  await Promise.resolve();
  console.log('中间'); // 进入微任务队列
  await Promise.resolve();
  console.log('结束'); // 进入微任务队列
}

test();
console.log('外部');

// 输出: 开始 -> 外部 -> 中间 -> 结束

错误处理

async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error('网络请求失败');
    }
    return await response.json();
  } catch (error) {
    console.error('错误:', error);
    // 可以选择重新抛出或返回默认值
    throw error;
  }
}

// 或者使用.catch
fetchData().catch(error => console.error(error));

这里的 return await response.json() 有明确目的:JSON 解析的 Promise 拒绝时,当前 catch 能接住它。如果写 return response.json(),函数已交出结果,后续拒绝只会传给调用者,当前 try/catch 不会捕获;但调用 response.json() 本身的同步异常仍能被捕获。

结果作用域与捕获范围

try { const result = await load(); } 中的 result 只在该代码块内可见,这是 const/let 的块级作用域规则,与是否异步无关。如何把结果交给后续代码,应同时考虑结果的作用域和谁负责处理哪类错误。

写法 适用情况 必须明确的边界
后续处理也放在 try 中 请求、解析、渲染失败共用同一种处理策略 catch 也会接住后续同步处理或被 await 的异步处理的异常
在外层声明 let result,在 try 中赋值 想把请求与后续处理分开 catch 必须返回、抛出或赋默认值,避免失败后继续使用未赋值结果;TypeScript 可通过控制流检查此问题
把加载封装成 async 辅助函数或 IIFE 外层保留 const,加载失败有明确回退 返回 null 等默认值会把失败转成成功,调用者必须处理这个分支

例如只给读取资料失败提供回退,渲染错误继续交给调用方:

async function showProfile(loadProfile, render, report) {
  const profile = await (async () => {
    try {
      return await loadProfile();
    } catch (error) {
      report(error);
      return null;
    }
  })();

  if (profile === null) return 'unavailable';
  return render(profile); // 不在上面的 catch 范围内。
}

showProfile(
  () => Promise.resolve({ name: 'Ada' }),
  profile => `Hello, ${profile.name}`,
  console.error
).then(console.log); // 'Hello, Ada'

示例约定加载成功返回资料对象,失败回退为 null;如果成功结果也可能是 null,应返回 { ok: true, value } / { ok: false, error } 这样的可区分结果。重复出现同类 IIFE 时抽成有业务含义的加载函数即可,不必为每次 await 增加包装层。

await loadProfile().catch(...) 也可缩小 Promise 拒绝的处理范围,但 loadProfile() 若在返回 Promise 之前同步抛错,尚未挂上的 .catch() 接不到它。上面的 try 同时覆盖函数调用和 await 等待;若错误应由上层处理,直接抛出,不要只记录后静默继续。

并行执行

// 两个独立请求串行启动,等待时间累加
async function getDataSlow() {
  const users = await fetchUsers();  // 等待2秒
  const posts = await fetchPosts();  // 再等待2秒
  return { users, posts }; // 总共4秒
}

// 两个独立请求同时启动,等待时间可重叠
async function getDataFast() {
  const [users, posts] = await Promise.all([
    fetchUsers(),
    fetchPosts()
  ]);
  return { users, posts }; // 总共2秒
}

最佳实践

  1. 使用try-catch处理错误:async/await让错误处理更像同步代码
  2. 避免不必要的await:独立的异步操作应该并行执行
  3. 注意循环中的await:在循环中使用await会导致串行执行
// 串行执行:适合顺序依赖或顺序副作用
async function processItems(items) {
  for (const item of items) {
    await processItem(item); // 一个一个处理
  }
}

// 并行执行:适合彼此独立且数量适中的任务
async function processItems(items) {
  await Promise.all(items.map(item => processItem(item)));
}

// 如果需要控制并发数
async function processItemsWithLimit(items, limit = 5) {
  const results = [];
  for (let i = 0; i < items.length; i += limit) {
    const batch = items.slice(i, i + limit);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    results.push(...batchResults);
  }
  return results;
}

以上按批次等待会被每批最慢的任务阻挡;这是简单的分批执行方式。串行适合存在前后依赖、顺序副作用或速率限制的场景,独立任务才适合并行。持续补充空闲并发槽的任务池可复用 JavaScript 基础中的并发控制。

面试要点

  1. async函数返回值:总是返回Promise对象(深入阅读:async/await的执行流程)
  2. await的阻塞特性:阻塞async函数内部代码,但不阻塞主线程(深入阅读:await会阻塞什么)
  3. 执行顺序:理解同步代码、微任务、宏任务的执行顺序(深入阅读:async/await的执行流程)
  4. 错误处理:掌握try-catch和.catch的使用(深入阅读:错误处理)
  5. 性能优化:避免不必要的串行await,使用Promise.all并行执行(深入阅读:并行执行)

经典面试题

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

async1();

new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});

console.log('script end');

深入阅读:详细解答

输出顺序:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解析:

  1. script start(同步代码)
  2. async1 start(同步代码)
  3. async2(await先执行右侧函数)
  4. promise1(Promise构造函数同步执行)
  5. script end(同步代码)
  6. async1 end(await后的代码进入微任务)
  7. promise2(then回调微任务)
  8. setTimeout(宏任务)

来源与取舍

  • 《JS异步编程.pdf》(FE-005)第 9-11 页:已有 async 返回值、执行顺序和基础 try/catch 直接复用;补充非 Promise 等待、块级作用域、捕获范围和 IIFE 回退方案。
  • 将来源中的「async 本质就是 Generator」改为控制流类比;作用域外不可见由块级声明造成,外层 let 可通过明确失败分支和 TypeScript 控制流检查安全使用。
  • PDF 的回退示例补上调用者对 null 的处理,以及 return await、同步抛错和后续渲染错误的边界,避免吞错后继续执行。