14. 说说你对async/await的理解?
理解 async/await 的返回值与微任务恢复,按错误责任划分 try/catch 的作用域,并选择串行或并行执行。
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
执行过程分析:
- 执行fn1,打印1
- 遇到await fn2(),先执行fn2,打印fn2
- await阻塞后面的代码,将其加入微任务队列
- 跳出async函数,执行同步代码,打印3
- 同步代码执行完毕,执行微任务,打印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秒
}
最佳实践
- 使用try-catch处理错误:async/await让错误处理更像同步代码
- 避免不必要的await:独立的异步操作应该并行执行
- 注意循环中的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 基础中的并发控制。
面试要点
- async函数返回值:总是返回Promise对象(深入阅读:async/await的执行流程)
- await的阻塞特性:阻塞async函数内部代码,但不阻塞主线程(深入阅读:await会阻塞什么)
- 执行顺序:理解同步代码、微任务、宏任务的执行顺序(深入阅读:async/await的执行流程)
- 错误处理:掌握try-catch和.catch的使用(深入阅读:错误处理)
- 性能优化:避免不必要的串行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
解析:
- script start(同步代码)
- async1 start(同步代码)
- async2(await先执行右侧函数)
- promise1(Promise构造函数同步执行)
- script end(同步代码)
- async1 end(await后的代码进入微任务)
- promise2(then回调微任务)
- setTimeout(宏任务)
来源与取舍
- 《JS异步编程.pdf》(FE-005)第 9-11 页:已有 async 返回值、执行顺序和基础
try/catch直接复用;补充非 Promise 等待、块级作用域、捕获范围和 IIFE 回退方案。 - 将来源中的「async 本质就是 Generator」改为控制流类比;作用域外不可见由块级声明造成,外层
let可通过明确失败分支和 TypeScript 控制流检查安全使用。 - PDF 的回退示例补上调用者对
null的处理,以及return await、同步抛错和后续渲染错误的边界,避免吞错后继续执行。