5月27日 23:41

async/await 的执行原理是什么?与 Promise 和事件循环有什么关系?

async/await 是 ES2017 引入的异步编程语法,本质上基于 Promise 和 Generator 实现。理解它的工作原理,关键在于弄清 await 做了什么、代码到底在哪一步暂停、以及它与事件循环中微任务队列的关系。

async 函数的返回值

async 函数无论内部返回什么,调用它拿到的永远是一个 Promise。返回普通值会被 Promise.resolve() 包装,抛出异常则对应一个 rejected 的 Promise。

javascript
async 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 的执行分两步:

  1. 立即求值 await 右侧的表达式。如果右侧不是 Promise,则用 Promise.resolve() 包装。
  2. 暂停当前 async 函数的执行,将 await 之后的代码注册为该 Promise 的 then 回调——即放入微任务队列。

注意:await 不会阻塞整个 JavaScript 主线程,它只暂停自己所在的 async 函数。外部调用栈会继续往下执行。

javascript
async 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 等

执行规则:每执行完一个宏任务,就会清空整个微任务队列,然后再执行下一个宏任务。

javascript
console.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 捕获

javascript
async 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:

javascript
async 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 不会因某个请求失败而中断,适合需要全部结果(含失败)的场景:

javascript
async 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 函数内

javascript
function 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 后精准捕获。

标签:Promise