异步编程:从“回调地狱”到“async/await”的救赎之路
JavaScript是单线程的但它却能同时处理很多事情。这是怎么做到的今天我们就来聊聊异步编程看看JS是怎么一边听歌一边刷网页的。从最原始的回调函数到Promise再到优雅的async/await这不仅是技术的演进更是一场“程序员不熬夜”的运动。前言你有没有经历过这种绝望写了一个网络请求结果后面的代码先执行了请求的数据还没回来页面已经渲染完了一片空白。或者你见过这样的代码getUser(function(user){getOrders(user.id,function(orders){getOrderDetails(orders[0].id,function(details){getProductInfo(details.productId,function(product){console.log(product);});});});});这就是传说中的回调地狱——代码像楼梯一样往右歪看得人头晕眼花。今天我们就来走一遍JS异步编程的进化史看看前辈们是怎么从地狱里爬出来的。一、为什么需要异步JavaScript是单线程的也就是说同一时间只能做一件事。如果所有事情都排队等着那遇到一个耗时操作比如网络请求、读取文件整个页面就得卡住用户点哪儿都没反应。异步就是解决方案遇到耗时操作先丢给浏览器或Node去“慢慢做”JS主线程继续执行后面的代码。等耗时操作完成了再通知JS“嘿我完事了你处理一下结果吧。”这就好比你点外卖你不会站在店门口干等一小时而是该干嘛干嘛等外卖小哥打电话叫你你再去取餐。异步就是这种“不干等”的机制。二、回调函数异步的原始形态回调函数是最早的异步解决方案把一个函数作为参数传给另一个函数等异步操作完成后调用这个函数。functionfetchData(callback){setTimeout((){callback(数据来了);},1000);}fetchData(function(data){console.log(data);// 一秒后输出数据来了});看起来还行对吧但一旦有多个依赖的异步操作就出事了。回调地狱长什么样// 先获取用户getUser(function(user){// 再根据用户ID获取订单getOrders(user.id,function(orders){// 再获取第一个订单的详情getOrderDetails(orders[0].id,function(details){// 再根据商品ID获取商品信息getProductInfo(details.productId,function(product){// 终于拿到了console.log(product);});});});});代码往右飞一眼看不到头。这还没算错误处理——每个回调都要处理错误代码量直接翻倍。这种代码别说维护了写的时候自己都要绕晕。回调的痛点嵌套太深代码可读性差错误处理困难每个回调都要try-catch难以并行执行多个异步操作三、Promise打破地狱的“链式反应”ES6引入了Promise它像是一个“承诺”现在还没有结果但将来一定会有要么成功要么失败。constpromisenewPromise((resolve,reject){setTimeout((){resolve(数据来了);// 如果出错reject(错误信息)},1000);});promise.then(data{console.log(data);}).catch(error{console.error(error);});Promise最大的好处是链式调用可以把嵌套的异步操作拍平getUser().then(usergetOrders(user.id)).then(ordersgetOrderDetails(orders[0].id)).then(detailsgetProductInfo(details.productId)).then(productconsole.log(product)).catch(errorconsole.error(error));看从“右飞”变成了“下飞”代码清晰多了。Promise的几个关键点状态不可逆Promise有三种状态pending进行中、fulfilled成功、rejected失败。一旦从pending变成fulfilled或rejected就不能再变了。链式传递then返回的是一个新的Promise所以可以一直链下去。错误冒泡只要链尾有一个catch前面任何一个环节出错都会落进来。并行操作Promise.all等待所有完成Promise.race等待最快的一个。// 并行请求Promise.all([fetchUser(),fetchOrders(),fetchProduct()]).then(([user,orders,product]){console.log(全部完成,user,orders,product);});Promise解决了回调地狱的问题但还是有些繁琐——你需要写很多.then和.catch而且处理复杂的逻辑时还是有点绕。四、async/await异步代码同步写ES2017推出的async/await是Promise的语法糖让异步代码看起来像同步代码一样直观。asyncfunctiongetProductInfo(){try{constuserawaitgetUser();constordersawaitgetOrders(user.id);constdetailsawaitgetOrderDetails(orders[0].id);constproductawaitgetProductInfo(details.productId);console.log(product);}catch(error){console.error(error);}}关键点async标记的函数返回一个Promiseawait后面跟一个Promise它会“暂停”函数执行直到Promise出结果错误处理直接用try/catch和同步代码一模一样这感觉就像终于可以用写同步代码的姿势写异步了不用再管什么then、catch代码一下子就清爽了。但注意await会阻塞函数内部但不阻塞外部asyncfunctiontest(){console.log(1);awaitnewPromise(resolvesetTimeout(resolve,1000));console.log(2);// 一秒后才输出}console.log(3);test();console.log(4);// 输出顺序1,3,4,一秒后2await只阻塞它所在的async函数外面的代码照常执行。这正是异步的精髓不干等。五、事件循环异步背后的幕后黑手说了这么多你有没有想过一个问题异步操作完成之后回调是怎么被调用的这就要提到**事件循环Event Loop**了。JS的执行机制大概是这样的主线程执行同步代码遇到异步任务比如setTimeout、网络请求就交给Web APIs浏览器或libuvNode去处理。异步任务完成后回调函数被放入任务队列。主线程的同步代码执行完后会不断从任务队列里取回调来执行。这个过程不断重复就是事件循环。任务队列还分宏任务和微任务宏任务setTimeout、setInterval、I/O操作、UI渲染微任务Promise.then、MutationObserver、queueMicrotask执行顺序是一个宏任务 → 所有微任务 → 渲染如果有 → 下一个宏任务。console.log(1);setTimeout(()console.log(2),0);Promise.resolve().then(()console.log(3));console.log(4);// 输出1,4,3,2为什么同步代码先执行1,4→ 微任务Promise.then3→ 下一个宏任务setTimeout2。六、实战封装一个带超时的fetch我们来用async/await封装一个实用的网络请求函数asyncfunctionfetchWithTimeout(url,timeout5000){constcontrollernewAbortController();consttimeoutIdsetTimeout(()controller.abort(),timeout);try{constresponseawaitfetch(url,{signal:controller.signal});clearTimeout(timeoutId);if(!response.ok){thrownewError(HTTP${response.status});}returnawaitresponse.json();}catch(error){if(error.nameAbortError){thrownewError(请求超时);}throwerror;}}// 使用try{constdataawaitfetchWithTimeout(https://api.example.com/data,3000);console.log(data);}catch(error){console.error(error.message);}这个函数既支持超时控制又有完善的错误处理用起来就像同步代码一样简单。七、异步编程的最佳实践能用async/await就用比原生Promise更易读错误处理也更自然。避免“忘掉await”忘记await会得到一个Promise对象而不是实际值这个bug很难找。并行任务用Promise.all如果多个异步任务互不依赖用Promise.all并行执行而不是挨个await。// 慢串行执行总耗时2秒constuserawaitgetUser();constordersawaitgetOrders();// 快并行执行总耗时1秒如果每个请求1秒const[user,orders]awaitPromise.all([getUser(),getOrders()]);错误处理要完整async/await用try/catchPromise用.catch()不要漏掉。避免在循环里用await除非你确实需要串行执行否则可以用Promise.all或for…of配合异步。// 这样会串行执行很慢for(constidofids){constitemawaitfetchItem(id);items.push(item);}// 并行执行快很多constitemsawaitPromise.all(ids.map(idfetchItem(id)));八、总结从地狱到天堂JS异步编程的演进史就是一部程序员与复杂性抗争的历史回调函数原始但容易陷入地狱Promise链式调用打破嵌套async/await让异步代码回归同步的直觉现在你应该能理解为什么异步这么重要以及怎么优雅地处理异步了。记住不要在回调里写回调不要在地狱里挣扎用Promise和async/await解救自己。明天我们将深入JS的另一座大山——事件循环Event Loop彻底搞懂微任务、宏任务、渲染时机这些核心概念。到时候你会发现那些让人头疼的异步面试题不过是一层窗户纸。如果你觉得今天的异步进化史讲得通透点个赞让更多人看到。有疑问评论区见我们明天见
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2446055.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!