async/await 的执行原理是什么?与 Promise 和事件循环有什么关系?
async/await 是 ES2017 引入的异步编程语法,本质上基于 Promise 和 Generator 实现。理解它的工作原理,关键在于弄清 await 做了什么、代码到底在哪一步暂停、以及它与事件循环中微任务队列的关系。
async 函数的返回值
async 函数无论内部返回什么,调用它拿到的永远是一个 Promise。返回普通值会被 Promise.resolve() 包装,抛出异常则对应一个 rejected 的 Promise。
javascriptasync function foo() { return 42; } foo(); // Promise { fulfilled: 42 } async function bar() { throw new Error("fail"); } bar(); // Promise { rejected: Error: fail }
这一点是后续理解执行流程的前提:async 函数本身并不异步执行,函数体内 await 之前的代码是同步运行的,只有遇到 await 才会产生暂停效果。
await 到底做了什么
await 的执行分两步:
- 立即求值 await 右侧的表达式。如果右侧不是 Promise,则用 Promise.resolve() 包装。
- 暂停当前 async 函数的执行,将 await 之后的代码注册为该 Promise 的 then 回调——即放入微任务队列。
注意:await 不会阻塞整个 JavaScript 主线程,它只暂停自己所在的 async 函数。外部调用栈会继续往下执行。
javascriptasync function demo() { console.log(1); await Promise.resolve(); console.log(2); } console.log("a"); demo(); console.log("b"); // 输出顺序: a → 1 → b → 2
为什么是 a → 1 → b → 2?console.log("a") 同步执行;调用 demo() 进入函数体,console.log(1) 同步执行;遇到 await,后面的 console.log(2) 被放入微任务队列,函数返回一个 pending 的 Promise;回到调用栈继续执行 console.log("b");同步代码跑完后,事件循环检查微任务队列,执行 console.log(2)。
事件循环与微任务队列
JavaScript 的事件循环模型决定了 async/await 的执行时序:
- 宏任务:script 整体代码、setTimeout、setInterval、I/O 回调等
- 微任务:Promise.then/catch/finally、await 之后的代码、queueMicrotask 等
执行规则:每执行完一个宏任务,就会清空整个微任务队列,然后再执行下一个宏任务。
javascriptconsole.log("script start"); setTimeout(() => console.log("setTimeout"), 0); Promise.resolve() .then(() => console.log("promise1")) .then(() => console.log("promise2")); async function async1() { console.log("async1 start"); await async2(); console.log("async1 end"); } async function async2() { console.log("async2"); } async1(); console.log("script end"); // 输出顺序: // script start → async1 start → async2 → script end → // promise1 → async1 end → promise2 → setTimeout
解析:同步代码先执行完毕;微任务队列中 promise1 先入队,async1 end 随后入队(因为 await async2() 右侧同步执行完后,await 后的代码才入微任务),所以 promise1 先输出,接着 async1 end,然后 promise1.then 产生 promise2 再执行;最后才轮到宏任务 setTimeout。
async/await 与 Promise 的等价转换
async/await 是 Promise 的语法糖,每一段 async/await 代码都可以机械地改写为 Promise 链式调用:
javascript// async/await 写法 async function fetchUser() { try { const res = await fetch("/api/user"); const data = await res.json(); return data; } catch (e) { console.error(e); throw e; } } // 等价 Promise 写法 function fetchUser() { return fetch("/api/user") .then(res => res.json()) .then(data => data) .catch(e => { console.error(e); throw e; }); }
V8 引擎在早期版本中将 async/await 编译为基于 Generator 的状态机(配合 __awaiter 辅助函数),现代 V8 已优化为直接生成 Promise 链,减少了 Generator 中间层带来的性能开销。
错误处理
try/catch 捕获
javascriptasync function fetchData() { try { const res = await fetch("/api/data"); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (e) { console.error("请求失败:", e.message); throw e; } }
try/catch 能否捕获未 await 的 Promise 异常?
不能。如果忘记 await,异常是 Promise 的 rejection,不会冒泡到外层 try/catch:
javascriptasync function foo() { try { asyncFnThatThrows(); // 没有 await,异常丢失 } catch (e) { // 捕获不到 } }
必须加 await 或手动 .catch() 才能捕获。
并发控制
顺序 vs 并行
多个独立的异步操作不要用 await 逐个等待,应使用 Promise.all 并行执行:
javascript// 顺序执行 — 慢 async function sequential() { const a = await fetchA(); // 等 1s const b = await fetchB(); // 再等 1s,总计 ~2s } // 并行执行 — 快 async function parallel() { const [a, b] = await Promise.all([fetchA(), fetchB()]); // 总计 ~1s }
容错并行
Promise.allSettled 不会因某个请求失败而中断,适合需要全部结果(含失败)的场景:
javascriptasync function fetchAll() { const results = await Promise.allSettled([ fetchUser(), fetchPosts(), fetchComments() ]); results.forEach(r => { if (r.status === "fulfilled") console.log(r.value); else console.error(r.reason); }); }
常见陷阱
在循环中顺序 await
javascript// 慢 — 逐个等待 for (const url of urls) { const data = await fetch(url); process(data); } // 快 — 并发请求 const results = await Promise.all(urls.map(u => fetch(u))); results.forEach(process);
在顶层直接使用 await
ES2022 引入了 Top-level await,在 ES Module 的顶层可以直接使用 await,但 CommonJS 模块中仍需包裹在 async 函数内。
await 只能用在 async 函数内
javascriptfunction foo() { await bar(); // SyntaxError }
面试追问
Q: async 函数中 await 一个非 Promise 值会怎样? 会自动用 Promise.resolve() 包装,等价于 await 一个立即 resolve 的 Promise。await 之后的代码仍会进入微任务队列,在当前同步代码执行完后才运行。
Q: 为什么 await 后面的代码是微任务而不是宏任务? 因为 await 的语义是等待 Promise 完成后继续执行,这个继续执行本质上就是 Promise 的 then 回调,而 Promise.then 属于微任务。如果放在宏任务队列中,每轮事件循环只会执行一个宏任务,延迟过高且不符合语义。
Q: async/await 相比 Promise.then 链式调用有什么不足? 两个主要局限:一是无法方便地实现 Promise.race/all 等组合逻辑,仍需回到 Promise API;二是 try/catch 无法区分错误来源,而 .catch() 可以在特定 .then 后精准捕获。