async/await 的原理是什么?怎样用 Generator 实现自动执行?
async 函数可以理解为 Generator 加一个自动执行器:遇到 await 就暂停,等 Promise 完成后带着结果恢复执行,失败就在暂停处抛出错误。手写时用 Generator 的 yield 代替 await,执行器反复调用 next,把每次 yield 出来的值包成 Promise,完成后再把结果传回去。
简答
const user = await getUser()读起来像同步代码,实际上函数在这里暂停了,等getUser()返回的 Promise 完成,再带着结果继续往下走。MDN 的说法是,await会暂停函数的执行,直到 Promise 完成或失败,完成的值就成了await表达式的结果。能“暂停再恢复”的函数,JavaScript 里早就有了,就是 Generator。Generator 函数执行到
yield会停下来,外面调用next(value)才继续,而且传进去的value会成为yield表达式的结果。所以只要写一个执行器:每次 Generator 吐出一个 Promise,就等它完成,再用next(结果)把结果送回去,失败就用throw(错误)在暂停的地方抛出,Generator 就能像 async 函数一样运行。早期没有 async/await 时,co 这类库就是这样做的。可以把 async 函数理解为“Generator 加自动执行器”的语法糖,
yield换成了await,执行器由引擎内置,返回值包装成 Promise。要注意,这是理解原理的模型,不是规范里的实现方式。async 函数不需要 Generator 也能运行,引擎有自己的实现,而且 async 函数的错误和栈信息比手写执行器更友好。
手写自动执行器
function run(generatorFn, ...args) {
return new Promise((resolve, reject) => {
const iterator = generatorFn(...args);
function step(method, value) {
let result;
try {
result = iterator[method](value);
} catch (error) {
reject(error);
return;
}
if (result.done) {
resolve(result.value);
return;
}
Promise.resolve(result.value).then(
(data) => step('next', data),
(error) => step('throw', error),
);
}
step('next', undefined);
});
}
const delay = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const result = run(function* () {
const a = yield delay(1, 10);
const b = yield delay(2, 10);
try {
yield Promise.reject(new Error('失败'));
} catch (error) {
return `${a + b},捕获到${error.message}`;
}
});
result.then((value) => console.log(value)); // 3,捕获到失败
第一次调用 step('next', undefined),Generator 开始执行,到第一个 yield 停下,吐出 delay(1, 10) 这个 Promise。执行器等它完成,拿到 1,调用 iterator.next(1),于是 a 就是 1。这正是 const a = await delay(1, 10) 的效果。
yield Promise.reject(...) 那一步,Promise 失败了,执行器调用 iterator.throw(error)。错误在 Generator 暂停的那一行抛出,被里面的 try...catch 捕获,和 async 函数里 try...catch 包住 await 的效果一样。
为什么要用 Promise.resolve(result.value) 包一层?因为 yield 后面不一定是 Promise,也可能是普通值。包一层以后,普通值会马上完成,处理方式就统一了,这和 await 普通值 的行为一致。
iterator[method](value) 外面为什么要 try...catch?如果 Generator 里的错误没被捕获,next 或 throw 调用本身会抛出,这时整个任务失败,要 reject 外层 Promise,相当于 async 函数返回的 Promise 失败了。
await 之后的代码什么时候执行
await 后面的代码总是在微任务里继续执行,哪怕等的是一个已经完成的值。所以:
async function f() {
console.log(1);
await null;
console.log(3);
}
f();
console.log(2);
输出顺序是 1、2、3。f() 同步执行到 await 前打印 1,然后暂停,把后续代码安排为微任务;外面继续执行,打印 2;当前同步代码执行完后,微任务执行,打印 3。事件循环的完整顺序见《事件循环中,任务和微任务按什么顺序执行?》。
常见追问
async 函数一定返回 Promise 吗?
一定。
return 1得到的是一个以 1 完成的 Promise,函数里抛出的错误会变成 Promise 失败。所以调用 async 函数时,同步的try...catch捕获不到它的错误,必须await它或者用.catch。
多个 await 写在一起会不会变慢?
如果几个请求互不依赖,写成连续的
await会一个等一个,总时间是相加的。应该先同时发起,再用await Promise.all([...])一起等,见《async/await 如何处理异常和并发?》。