Skip to main content

async/await

async/await 是 ES8 引入的语法糖,它的本质是 Generator 协程 + Promise 的自动执行器。它让我们能以写同步代码的思维去处理异步逻辑,解决了 Promise 链式调用带来的“回调地狱”和变量共享困难的问题。

核心特性对比与优势

与纯 Promise 相比,async/await 的核心优势不仅在于“少写了 .then”,更在于:

  • 更好的错误栈跟踪:在 try/catch 块中捕获异常时,错误栈会指向具体的代码行,而不是像 Promise 那样只抛出一个泛泛的未捕获异常。
  • 中间值共享极其自然:在 Promise 链中,如果步骤 C 需要用到步骤 A 的结果,往往要在外部作用域声明一个变量。而用 await 只需要写三个 const 即可。
  • 调试友好 (Step-over 体验):如果你在 await 的那一行打断点,你可以直接点击“下一步(Step over)”,调试器会稳稳地停在下一行代码。但如果你在包含多个 .then 的 Promise 链中打断点,由于它们是异步回调函数,点击“下一步”时调试器往往会直接跳出当前函数(甚至跳进内部源码),你必须要在每一个 .then 的回调内部手动打上断点才能追踪值的变化。

async 关键字

  • 强制返回 Promise:无论 async 函数内部 return 的是什么,引擎都会自动用 Promise.resolve() 把它包裹起来。
  • 异常捕获:如果函数内部抛出错误(throw new Error)且没有被 try/catch 捕获,它会自动转为返回一个状态为 rejected 的 Promise,而不是阻断整个主线程。

await 关键字

  • 阻塞效果(宏观同步,微观异步)await 会暂停当前 async 函数体内部后续代码的执行,交出主线程的控制权。
  • 微任务调度await 后面的代码,实际上会被隐式地包裹进一个 Promise.then 的回调中,并推入微任务队列
  • 错误阻断:只要有一个 await 后面的 Promise 变成了 rejected,且没有被捕获,那么整个 async 函数就会立刻中断并抛出异常。
    • 实战技巧:如果你不想因为一个接口挂了就导致后续逻辑全盘崩溃,可以通过 try-catch 单独包裹这个 await,或者直接在后面跟一个 .catch() 吞掉错误:const res = await fetch().catch(() => null)

底层实现

这里的核心就是写一个自动执行器(Runner)。Generator 必须靠 next() 来一步步往下走,而自动执行器的作用就是:yield 吐出来的 Promise 的 then 回调,跟下一次的 next() 绑在一起。

// 1. 我们先写一段使用了 Generator 的异步流(模拟 async/await)
function* myAsync() {
const res1 = yield Promise.resolve(1);
console.log("第一步结果:", res1);

const res2 = yield Promise.resolve(res1 + 1);
console.log("第二步结果:", res2);

return res2 + 1;
}

// 2. 核心考点:手写自动执行器 runner
function runner(genFn) {
return new Promise((resolve, reject) => {
const generator = genFn(); // 初始化迭代器

// 定义一个递归调用的驱动函数
function step(nextFn) {
let nextResult;
try {
nextResult = nextFn(); // 执行 generator.next() 或 throw()
} catch (e) {
return reject(e); // 捕获生成器内部的同步错误
}

// 如果生成器执行完毕,直接 resolve 整个外层 Promise
if (nextResult.done) {
return resolve(nextResult.value);
}

// 如果还没执行完,把 yield 出来的值统一包装成 Promise
Promise.resolve(nextResult.value).then(
// 成功了,就把结果传回生成器,并递归触发下一步
(val) => step(() => generator.next(val)),
// 失败了,就把错误扔回给生成器内部的 try/catch
(err) => step(() => generator.throw(err)),
);
}

// 首次启动生成器
step(() => generator.next());
});
}

// 3. 测试运行
runner(myAsync).then((finalRes) => {
console.log("最终结果:", finalRes); // 3
});

扩展阅读:上述的 runner 函数,在早期 Node.js 社区中有一个极其著名的实现库,叫做 co(作者是 TJ 大神)。在 async/await 成为标准之前,社区就是靠 co + Generator 来愉快地编写异步代码的。