我读了一遍 Babel 编译后的 async/await,终于搞懂了它的原理(附 20 行手写实现)
本文从一个真实项目 bug 出发带你读 Babel 编译结果然后手写一个最简 async/await。1. 一个真实的“翻车”场景上周维护一个老项目看到同事写了这样的代码asyncfunctionprocessItems(items){constresults[];for(leti0;iitems.length;i){constresawaitfetch(/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;}constggen();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(){constaawaitPromise.resolve(1);constbawaitPromise.resolve(2);returnab;}用 Babelbabel/preset-env编译后简化版变成了类似这样的代码functiongetData(){return_asyncToGenerator(function*(){constayieldPromise.resolve(1);constbyieldPromise.resolve(2);returnab;})();}核心是_asyncToGenerator这个辅助函数——它接收一个generator 函数并返回一个自动执行该 generator 的函数最终返回一个 Promise。4. 手写核心自动执行器我们先写一个函数run(generatorFunc)它能自动执行 generator 直到结束。functionrun(generatorFunc){constgeneratorgeneratorFunc();// 获取迭代器对象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(){constayieldPromise.resolve(1);constbyieldPromise.resolve(2);returnab;}run(myGen).then(console.log);// 输出 3完美运行。上面的run就是_asyncToGenerator最核心的逻辑。真正的 Babel 实现还处理了更多边界情况但原理完全一致。5. 封装成真正的asyncToGenerator如果你想让函数直接返回 Promise可以这样封装functionasyncToGenerator(generatorFunc){returnfunction(...args){constgengeneratorFunc.apply(this,args);returnnewPromise((resolve,reject){functionstep(key,arg){letresult;try{resultgen[key](arg);}catch(err){returnreject(err);}const{value,done}result;if(done){resolve(value);}else{Promise.resolve(value).then(vstep(next,v),estep(throw,e));}}step(next);});};}用法constgetDataasyncToGenerator(function*(){constayieldPromise.resolve(1);constbyieldPromise.resolve(2);returnab;});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(idfetch(/api/${id})));理解原理后你就知道为什么串行会慢以及什么时候该用Promise.all。7. 总结async/await的底层 generator 自动执行器手写一个自动执行器只需 20 行左右真正理解原理后你就能轻松避免“异步陷阱”文中代码可以直接复制到你的项目中跑一跑讨论你在项目中遇到过哪些因不理解 async/await 原理而产生的 bug欢迎在评论区分享你的“翻车”经历