我读了一遍 Babel 编译后的 async/await,终于搞懂了它的原理(附 20 行手写实现)
2026/6/8 19:42:50 网站建设 项目流程

本文从一个真实项目 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?欢迎在评论区分享你的“翻车”经历~

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询