本文从一个真实项目 bug 出发,带你读 Babel 编译结果,然后手写一个最简 async/await。
1. 一个真实的"翻车"场景
上周维护一个老项目,看到同事写了这样的代码:
javascript
async function processItems(items) {
const results = [];
for (let i = 0; i < items.length; i++) {
const res = await fetch(`/api/process/${items[i]}`);
results.push(res);
}
return results;
}
他把 await 放在 for 循环里,本意是串行 请求,结果因为接口响应时间不同,数据顺序全乱了。我帮他改成 Promise.all 后,突然意识到:我其实并不清楚 async/await 底层到底怎么工作的。
于是我去看了 Babel 把 async 函数编译成了什么样子------发现它只是一个 generator + 自动执行器 的包装。
这篇文章,我就用 20 行代码 ,带你手写一个最简版的 async/await。
2. 前置知识:Generator 函数
如果你已经熟悉 generator,可以跳过本节。
Generator 是可以暂停和恢复的函数:
javascript
function* gen() {
console.log('step 1');
yield 1;
console.log('step 2');
yield 2;
return 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: true }
每次调用 next(),函数会执行到下一个 yield 并暂停。
这个特性正好可以用来模拟 await 的"等待异步结果再继续"的行为。
3. Babel 编译后长什么样?
写一个最简单的 async 函数:
javascript
async function getData() {
const a = await Promise.resolve(1);
const b = await Promise.resolve(2);
return a + b;
}
用 Babel(@babel/preset-env)编译后(简化版),变成了类似这样的代码:
javascript
function getData() {
return _asyncToGenerator(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
})();
}
核心是 _asyncToGenerator 这个辅助函数------它接收一个 generator 函数 ,并返回一个自动执行该 generator 的函数,最终返回一个 Promise。
4. 手写核心:自动执行器
我们先写一个函数 run(generatorFunc),它能自动执行 generator 直到结束。
javascript
function run(generatorFunc) {
const generator = generatorFunc(); // 获取迭代器对象
return new Promise((resolve, reject) => {
function step(nextFunc) {
try {
const { value, done } = nextFunc();
if (done) {
resolve(value);
} else {
// 确保 value 是一个 Promise
Promise.resolve(value).then(
(res) => step(() => generator.next(res)),
(err) => step(() => generator.throw(err))
);
}
} catch (err) {
reject(err);
}
}
step(() => generator.next()); // 启动执行
});
}
测试一下:
javascript
function* myGen() {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
}
run(myGen).then(console.log); // 输出 3
完美运行。
上面的
run就是_asyncToGenerator最核心的逻辑。真正的 Babel 实现还处理了更多边界情况,但原理完全一致。
5. 封装成真正的 asyncToGenerator
如果你想让函数直接返回 Promise,可以这样封装:
javascript
function asyncToGenerator(generatorFunc) {
return function(...args) {
const gen = generatorFunc.apply(this, args);
return new Promise((resolve, reject) => {
function step(key, arg) {
let result;
try {
result = gen[key](arg);
} catch (err) {
return reject(err);
}
const { value, done } = result;
if (done) {
resolve(value);
} else {
Promise.resolve(value).then(
v => step('next', v),
e => step('throw', e)
);
}
}
step('next');
});
};
}
用法:
javascript
const getData = asyncToGenerator(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(2);
return a + b;
});
getData().then(console.log); // 3
和原生 async/await 行为完全一致。
6. 常见误解与踩坑
6.1 await 后面跟着的不是 Promise 会怎样?
await 123 会被隐式转换为 await Promise.resolve(123),所以自动执行器里用 Promise.resolve(value) 包裹是正确的。
6.2 异步错误怎么捕获?
如果 generator 内部 yield 了一个 rejected Promise,自动执行器会调用 generator.throw(err),然后在 try-catch 中 reject 最终的 Promise。所以外层的 .catch 可以捕获。
6.3 for 循环里的 await 是串行还是并行?
javascript
// 串行(一个接一个)
for (const id of ids) {
await fetch(`/api/${id}`);
}
// 并行(同时发起)
await Promise.all(ids.map(id => fetch(`/api/${id}`)));
理解原理后,你就知道为什么串行会慢,以及什么时候该用 Promise.all。
7. 总结
async/await的底层 = generator + 自动执行器- 手写一个自动执行器只需 20 行左右
- 真正理解原理后,你就能轻松避免"异步陷阱"
- 文中代码可以直接复制到你的项目中跑一跑
讨论:你在项目中遇到过哪些因不理解 async/await 原理而产生的 bug?欢迎在评论区分享你的"翻车"经历~