js异步编程的演变:回调函数、Promise、async/await(代码原理演示)
作者:wx66ece9f42611c
前端开发里的异步场景:比如先请求“用户信息”,再用用户ID请求“订单列表”,最后用订单ID请求“订单详情”,同时不能让页面卡住(比如按钮点不动、滚动不流畅)。而 async/await 就是解决这种场景的“最优解”——让异步代码写起来像“等快递”一样顺理成章,却不会阻塞主线程。
一、异步编程的“进化史”:从“乱糟糟”到“清爽”
在async/await出现前,前端工程师为了处理异步任务,踩过不少坑。我们以“按顺序请求三个API”为例,看看每一代方案的问题。
1. 第一代:回调地狱(Callback Hell)——嵌套到“头皮发麻”
最原始的异步处理靠回调函数,比如用setTimeout
模拟API请求:
// 模拟API请求:根据参数返回数据,1秒后执行回调 function requestData(url, callback) { setTimeout(() => { // 模拟返回数据(比如用户信息、订单等) const data = `来自${url}的数据`; callback(null, data); // 成功回调:err为null,data为结果 }, 1000); } // 需求:先请求用户信息,再请求订单,最后请求详情 requestData("/api/user", (err, userData) => { if (err) return console.error("用户请求失败", err); console.log("拿到用户数据:", userData); // 用用户数据里的userId请求订单 requestData(`/api/orders?userId=${userData.id}`, (err, orderData) => { if (err) return console.error("订单请求失败", err); console.log("拿到订单数据:", orderData); // 用订单id请求详情 requestData(`/api/orderDetail?orderId=${orderData.id}`, (err, detailData) => { if (err) return console.error("详情请求失败", err); console.log("拿到详情数据:", detailData); }); }); });
问题显而易见:
- 嵌套金字塔:每多一个异步任务,就多一层嵌套,代码像“千层饼”,后期维护时找bug要“逐层扒”;
- 错误处理麻烦:每个回调里都要写
if(err)
,一旦漏写就可能导致报错无法捕获; - 逻辑分散:“先做A、再做B、最后做C”的逻辑被拆在不同回调里,阅读时要“跳着看”。
2. 第二代:Promise 链式调用——“扁平”但仍需“手动衔接”
为了解决回调地狱,Promise应运而生:它把异步任务包装成一个“容器”,用.then()
链式调用,让代码变扁平:
// 用Promise重写请求函数:不再需要回调,直接返回Promise function requestDataPromise(url) { return new Promise((resolve) => { setTimeout(() => { const data = `来自${url}的数据`; resolve(data); // 异步成功后,用resolve返回结果 }, 1000); }); } // 链式调用:按顺序请求 requestDataPromise("/api/user") .then((userData) => { console.log("拿到用户数据:", userData); // 返回下一个Promise,衔接下一个.then return requestDataPromise(`/api/orders?userId=${userData.id}`); }) .then((orderData) => { console.log("拿到订单数据:", orderData); return requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`); }) .then((detailData) => { console.log("拿到详情数据:", detailData); }) .catch((err) => { // 集中捕获所有环节的错误,不用每个步骤写if(err) console.error("某个请求失败:", err); });
进步很大,但仍有瑕疵:
- 虽然扁平了,但需要频繁写
.then()
,代码里穿插大量“衔接符”,不够直观; - 逻辑顺序还是“链式的”,如果任务多,
.then()
链条会很长,阅读时要“顺着链条找”。
3. 第三代:async/await——“像写同步代码一样写异步”
终于到了async/await出场的时候。它是Promise的“语法糖”,但把异步代码的可读性拉到了顶峰:
// 用async/await重写:逻辑顺序和代码顺序完全一致 async function fetchAllData() { try { // 1. 先请求用户数据:await“等待”Promise完成,再往下走 const userData = await requestDataPromise("/api/user"); console.log("拿到用户数据:", userData); // 2. 用用户数据请求订单:自然衔接,像同步代码一样 const orderData = await requestDataPromise(`/api/orders?userId=${userData.id}`); console.log("拿到订单数据:", orderData); // 3. 用订单数据请求详情 const detailData = await requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`); console.log("拿到详情数据:", detailData); return "所有数据请求完成!"; } catch (err) { // 集中捕获所有await环节的错误,和同步代码的try/catch完全一致 console.error("请求失败:", err); } } // 调用async函数 console.log("程序开始执行"); const result = fetchAllData(); console.log("等待数据请求..."); // 这行会先执行,证明没有阻塞 result.then((msg) => console.log(msg));
输出顺序(关键!看“非阻塞”证明):
程序开始执行 等待数据请求... // 1秒后(第一个请求完成) 拿到用户数据:来自/api/user的数据 // 又1秒后(第二个请求完成) 拿到订单数据:来自/api/orders?userId=xxx的数据 // 再1秒后(第三个请求完成) 拿到详情数据:来自/api/orderDetail?orderId=xxx的数据 所有数据请求完成!
核心优势直接“封神”:
- 代码即逻辑:“先做A,再做B,最后做C”的逻辑,和同步代码写法完全一致,不用跳着看;
- 错误处理简单:用
try/catch
包裹所有异步步骤,和处理同步错误的方式一样; - 完全不阻塞:等待异步任务时,主线程会去执行其他代码(比如上面的“等待数据请求...”),页面不会卡住。
二、async/await 到底是“怎么干活”的?本质是 Generator 的“自动版”
很多人只知道async/await好用,却不知道它背后的“黑魔法”——其实它是 Generator函数 + Promise自动执行器 的封装。我们一步步拆解开看。
1. 先认识“半成品”:Generator函数
Generator函数是ES6引入的一种“可暂停、可恢复”的函数,用function*
定义,内部用yield
关键字暂停执行:
// Generator函数:处理三个异步请求 function* fetchGenerator() { // yield会暂停函数,返回后面的Promise;恢复时,把Promise的结果赋值给userData const userData = yield requestDataPromise("/api/user"); console.log("拿到用户数据:", userData); const orderData = yield requestDataPromise(`/api/orders?userId=${userData.id}`); console.log("拿到订单数据:", orderData); const detailData = yield requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`); console.log("拿到详情数据:", detailData); return "完成"; }
但Generator有个“缺点”:需要手动“驱动”它执行
Generator函数调用后不会直接执行,而是返回一个“迭代器(iterator)”,需要调用iterator.next()
才能让函数继续执行:
// 手动驱动Generator执行 const iterator = fetchGenerator(); // 第一次调用next():函数执行到第一个yield,返回{ value: Promise, done: false } iterator.next().value.then((userData) => { // 第二次调用next(userData):把userData传给第一个yield的左边,函数执行到第二个yield iterator.next(userData).value.then((orderData) => { // 第三次调用next(orderData):函数执行到第三个yield iterator.next(orderData).value.then((detailData) => { // 第四次调用next(detailData):函数执行完,done变为true iterator.next(detailData); }); }); });
你看,Generator已经实现了“暂停异步任务、按顺序执行”,但需要手动写嵌套的.then()
来驱动——这显然不够方便。
2. 给Generator加个“自动档”:写一个简单的自动执行器
既然手动驱动太麻烦,我们可以写一个函数,自动帮我们调用next()
,直到Generator执行完成:
// Generator自动执行器:接收Generator函数,自动驱动它完成 function runGenerator(generatorFunc) { // 1. 创建迭代器 const iterator = generatorFunc(); // 2. 递归调用next() function autoNext(value) { // 执行next(),拿到{ value: Promise, done: 布尔值 } const result = iterator.next(value); // 如果执行完了,就退出 if (result.done) return; // 如果没执行完,等待Promise完成后,递归调用autoNext result.value.then((data) => { autoNext(data); // 把Promise的结果传给下一个next() }).catch((err) => { iterator.throw(err); // 捕获错误,传给Generator的try/catch }); } // 启动自动执行 autoNext(); } // 现在,调用自动执行器就够了,不用手动写嵌套! runGenerator(fetchGenerator);
这下Generator终于“自动化”了——而 async/await 本质上就是把“Generator函数 + 自动执行器”封装成了更简洁的语法。
3. async/await 是怎么“封装”的?
我们可以把async function
理解为“自带自动执行器的Generator函数”,浏览器或Node.js内部帮我们做了这些事:
async
关键字:告诉引擎“这是一个需要自动执行的异步函数”,调用时会自动创建迭代器并驱动执行;await
关键字:相当于yield
的“语法糖”,自动等待后面的Promise完成,并把结果返回,不用手动处理next()
;- 错误处理:内部自动捕获Promise的
reject
状态,抛给外层的try/catch
,不用手动写iterator.throw()
。
简单来说:async/await = Generator函数 + 内置自动执行器 + 更友好的语法
。
三、关键误区:async/await 不是“同步”,而是“伪同步”
很多人用多了async/await,会误以为它是“同步代码”——但实际上它只是“看起来同步”,本质还是异步,不会阻塞主线程。我们用一个“事件循环”的例子来证明:
console.log("1. 全局同步代码开始"); // async函数 async function asyncDemo() { console.log("2. async函数内部同步代码"); // await后面的Promise会暂停函数,把后续代码放进“微任务队列” await Promise.resolve("模拟异步完成"); console.log("5. await之后的代码(微任务)"); } // 调用async函数 asyncDemo(); // 其他同步代码 console.log("3. 全局同步代码继续"); setTimeout(() => { console.log("6. setTimeout回调(宏任务)"); }, 0); console.log("4. 全局同步代码结束");
最终输出顺序:
1. 全局同步代码开始 2. async函数内部同步代码 3. 全局同步代码继续 4. 全局同步代码结束 5. await之后的代码(微任务) 6. setTimeout回调(宏任务)
为什么会这样?拆解执行流程:
- 主线程先执行所有同步代码:1→2→3→4;
- 遇到
await Promise.resolve()
时,引擎会:
- 暂停
asyncDemo
函数,把await
后面的代码(console.log("5..."))放进“微任务队列”; - 主线程继续执行其他同步代码(3→4);
- 同步代码执行完后,主线程会清空“微任务队列”,执行console.log("5...");
- 微任务执行完后,再执行“宏任务队列”里的setTimeout回调(6)。
这就证明了:await不会阻塞主线程,它只是让函数内部的代码“按顺序等”,主线程该干嘛干嘛——这就是“伪同步”的本质。
四、实际开发中的“避坑指南”
async/await虽好,但新手容易踩坑,分享两个高频注意点:
1. 并行任务别用“串行await”,用Promise.all
如果多个异步任务之间没有依赖(比如同时请求“商品列表”和“用户信息”),不要用多个await串行执行,会浪费时间:
// 错误写法:串行执行,总耗时=1秒+1秒=2秒 async function badFetch() { const goods = await requestDataPromise("/api/goods"); // 1秒 const user = await requestDataPromise("/api/user"); // 又1秒 console.log(goods, user); } // 正确写法:并行执行,总耗时=1秒(两个请求同时发) async function goodFetch() { // 用Promise.all同时发起多个请求,await等待所有请求完成 const [goods, user] = await Promise.all([ requestDataPromise("/api/goods"), requestDataPromise("/api/user") ]); console.log(goods, user); }
2. try/catch
的范围要“合理”
如果希望某个异步任务失败后,其他任务还能继续执行,不要把所有await都放进一个try/catch
:
async function fetchWithError() { try { const user = await requestDataPromise("/api/user"); // 假设这个请求失败 console.log("用户数据:", user); } catch (err) { console.error("用户请求失败:", err); // 只捕获用户请求的错误 } // 即使上面失败,这里仍会执行 const goods = await requestDataPromise("/api/goods"); console.log("商品数据:", goods); }
五、总结:async/await 的核心逻辑
1. 原理公式
async/await = Generator函数(暂停/恢复) + Promise自动执行器(驱动流程) + 事件循环(调度微任务)
2. 核心优势对比
特性 | 回调函数 | Promise链式 | async/await |
代码可读性 | 差(嵌套) | 中(链式) | 优(同步写法) |
错误处理 | 繁琐(每层判断) | 中(.catch()) | 优(try/catch) |
是否阻塞主线程 | 否 | 否 | 否 |
学习成本 | 低 | 中 | 中(需理解原理) |
并行任务处理 | 复杂(需手动管理) | 优(Promise.all) | 优(配合Promise.all) |
3. 面试怎么答?
“async/await是Promise的语法糖,本质基于Generator函数和自动执行器:
async
标记函数为异步,调用时自动创建迭代器并驱动执行;await
会暂停函数,等待后面的Promise完成后,把结果返回并恢复函数;- 它让代码看起来像同步,但实际是通过事件循环调度微任务实现异步,不会阻塞主线程。”
理解async/await,不只是会用,更要明白它是JavaScript异步编程“从复杂到简洁”的必然结果——它解决了“按顺序处理异步任务”的核心痛点,同时保留了异步的高效性,这也是它能成为前端开发“标配”的原因。
到此这篇关于js异步编程的演变:回调函数、Promise、async/await(代码原理演示)的文章就介绍到这了,更多相关js异步:回调函数、Promise、async/await内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!