本文从一个真实项目 bug 出发,带你读 Babel 编译结果,然后手写一个最简 async/await。
1. 一个真实的“翻车”场景
上周维护一个老项目,看到同事写了这样的代码:
asyncfunctionprocessItems(items){constresults=[];for(leti=0;i<items.length;i++){constres=awaitfetch(`/api/process/${items[i]}`);results.push(res);}returnresults;}他把await放在for循环里,本意是串行请求,结果因为接口响应时间不同,数据顺序全乱了。我帮他改成Promise.all后,突然意识到:我其实并不清楚async/await底层到底怎么工作的。
于是我去看了 Babel 把async函数编译成了什么样子——发现它只是一个generator + 自动执行器的包装。
这篇文章,我就用20 行代码,带你手写一个最简版的async/await。
2. 前置知识:Generator 函数
如果你已经熟悉 generator,可以跳过本节。
Generator 是可以暂停和恢复的函数:
function*gen(){console.log('step 1');yield1;console.log('step 2');yield2;return3;}constg=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函数:
asyncfunctiongetData(){consta=awaitPromise.resolve(1);constb=awaitPromise.resolve(2);returna+b;}用 Babel(@babel/preset-env)编译后(简化版),变成了类似这样的代码:
functiongetData(){return_asyncToGenerator(function*(){consta=yieldPromise.resolve(1);constb=yieldPromise.resolve(2);returna+b;})();}核心是_asyncToGenerator这个辅助函数——它接收一个generator 函数,并返回一个自动执行该 generator 的函数,最终返回一个 Promise。
4. 手写核心:自动执行器
我们先写一个函数run(generatorFunc),它能自动执行 generator 直到结束。
functionrun(generatorFunc){constgenerator=generatorFunc();// 获取迭代器对象returnnewPromise((resolve,reject)=>{functionstep(nextFunc){try{const{value,done}=nextFunc();if(done){resolve(value);}else{// 确保 value 是一个 PromisePromise.resolve(value).then((res)=>step(()=>generator.next(res)),(err)=>step(()=>generator.throw(err)));}}catch(err){reject(err);}}step(()=>generator.next());// 启动执行});}测试一下:
function*myGen(){consta=yieldPromise.resolve(1);constb=yieldPromise.resolve(2);returna+b;}run(myGen).then(console.log);// 输出 3完美运行。
上面的
run就是_asyncToGenerator最核心的逻辑。真正的 Babel 实现还处理了更多边界情况,但原理完全一致。
5. 封装成真正的asyncToGenerator
如果你想让函数直接返回 Promise,可以这样封装:
functionasyncToGenerator(generatorFunc){returnfunction(...args){constgen=generatorFunc.apply(this,args);returnnewPromise((resolve,reject)=>{functionstep(key,arg){letresult;try{result=gen[key](arg);}catch(err){returnreject(err);}const{value,done}=result;if(done){resolve(value);}else{Promise.resolve(value).then(v=>step('next',v),e=>step('throw',e));}}step('next');});};}用法:
constgetData=asyncToGenerator(function*(){consta=yieldPromise.resolve(1);constb=yieldPromise.resolve(2);returna+b;});getData().then(console.log);// 3和原生async/await行为完全一致。
6. 常见误解与踩坑
6.1await后面跟着的不是 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.3for循环里的await是串行还是并行?
// 串行(一个接一个)for(constidofids){awaitfetch(`/api/${id}`);}// 并行(同时发起)awaitPromise.all(ids.map(id=>fetch(`/api/${id}`)));理解原理后,你就知道为什么串行会慢,以及什么时候该用Promise.all。
7. 总结
async/await的底层 =generator + 自动执行器- 手写一个自动执行器只需 20 行左右
- 真正理解原理后,你就能轻松避免“异步陷阱”
- 文中代码可以直接复制到你的项目中跑一跑
讨论:你在项目中遇到过哪些因不理解 async/await 原理而产生的 bug?欢迎在评论区分享你的“翻车”经历~