异步代码怎么写才不翻车?前端仔的避坑实战指南
- 异步代码怎么写才不翻车?前端仔的避坑实战指南
- 为啥每次写异步都像在拆炸弹
- JS 单线程是怎么“假装”多线程的
- 回调函数:老祖宗的办法还能用吗
- Promise 登场:终于能链式调用了
- async/await:语法糖还是救命稻草
- Generator 函数:被时代遗忘的中间人
- 实际开发怎么选?看场景别死磕
- 异步代码调试太难?教你几招骚操作
- 那些年我们踩过的异步大坑
- 提升可读性的土办法
- 异步不是魔法,写清楚比写炫更重要
异步代码怎么写才不翻车?前端仔的避坑实战指南
微信群语音转文字,想到哪说到哪,语气碎、逻辑乱、前后打脸,但句句带血。
为啥每次写异步都像在拆炸弹
先坦白:我第一次用setTimeout的时候,以为它就是“多线程”,结果把按钮连点 10 下,页面直接卡成 PPT,老板还以为我偷偷挖矿。
后来学了回调,以为拿到免死金牌,结果嵌套七层之后,VSCode 右侧滚动条细成一根针,那一刻我深刻体会到什么叫“代码写完了,人也走远了”。
直到某天凌晨两点,测试妹子发来一句:“哥,下单接口怎么又超时?”
我才发现,异步这玩意儿,不会写=埋雷,会写=排雷,写得好=拆完雷还顺手给队友泡了面。
JS 单线程是怎么“假装”多线程的
先给萌新补一刀:JS 只有一个主线程,就像奶茶店只有一个店员,但排队的人里既有堂食又有外卖。
店员(主线程)不可能同时做两杯奶茶,于是把“加珍珠”“封盖”这些耗时的动作甩给后厨(WebAPI),后厨做完把订单贴到“取餐栏”(任务队列),店员把手头这杯做完再去取餐栏拿下一单——这就是事件循环。
宏任务 vs 微任务的江湖地位:
- 宏任务:setTimeout、setInterval、I/O、UI 渲染
- 微任务:Promise.then、queueMicrotask、MutationObserver
每跑完一个宏任务,JS 都会把当前一轮所有微任务先清空,再跑下一个宏任务。
说人话:宏任务像“大轮班”,微任务像“插号”。微任务可以无限插号,直到把队伍插满,才轮到下一个宏任务。
所以如果你写个死循环while(true) { Promise.resolve().then(()=>{}) },页面就直接原地去世,别问我是怎么知道的。
回调函数:老祖宗的办法还能用吗
能,但别嵌套。
回忆杀:Node 早期读取文件就长这样:
constfs=require('fs');fs.readFile('./a.txt','utf8',(err,a)=>{if(err)returnconsole.error(err);fs.readFile(`./${a.trim()}.txt`,'utf8',(err,b)=>{if(err)returnconsole.error(err);fs.readFile(`./${b.trim()}.txt`,'utf8',(err,c)=>{if(err)returnconsole.error(err);console.log('最终结果:',c);});});});三层就算“回调地狱”?这明明叫“回调十八层地狱”。
后来我用 jQuery 的$.ajax,依旧逃不掉,只不过把金字塔换成倒金字塔,看着更刺激。
错误处理全靠 if(err),一不小心就漏写,线上日志里一堆undefined is not a function,查错查到想剃度。
Promise 登场:终于能链式调用了
Promise 刚出来时,我激动得连夜把老项目重构,结果第二天测试环境炸了——因为我把return callback()直接换成resolve(),忘了reject,错误全静默,老板差点把我 resolve 掉。
正确姿势先记好:
functionfetchUser(id){returnnewPromise((resolve,reject)=>{// 别偷懒,两参数都写上$.ajax({url:`/user/${id}`,success:resolve,error:reject// 这里必须给,否则 404 也走 resolve});});}链式调用爽感堪比奶茶加 double 奶盖:
fetchUser(1).then(user=>fetchOrder(user.lastOrderId))// 返回 Promise.then(order=>fetchGoods(order.goodsId)).then(goods=>console.log('商品名:',goods.name)).catch(err=>console.error('任何一步失败都在这里',err));Promise.all并发请求,谁用谁香:
constids=[1,2,3];Promise.all(ids.map(id=>fetchUser(id))).then(users=>{// users 顺序与 ids 一一对应,哪怕 2 比 1 先返回console.log('批量用户:',users);}).catch(err=>{// 只要有一个失败,就全失败console.error('某个接口跪了',err);});但注意:Promise.all 一旦有一个 reject 就全挂。
业务上如果允许部分失败,请用Promise.allSettled,返回数组带状态:
constresults=awaitPromise.allSettled(ids.map(id=>fetchUser(id)));results.forEach(r=>{if(r.status==='fulfilled')console.log('成功:',r.value);elseconsole.warn('失败:',r.reason);});async/await:语法糖还是救命稻草
上面那句await Promise.allSettled其实已经偷偷用上了 async/await。
一句话总结:async 函数本质上就是把返回值包一层 Promise,await 就是拆开 Promise 的语法糖。
asyncfunctionshowGoods(userId){try{constuser=awaitfetchUser(userId);constorder=awaitfetchOrder(user.lastOrderId);constgoods=awaitfetchGoods(order.goodsId);console.log('商品名:',goods.name);}catch(err){// 任何一步抛错都会到这里console.error('出错:',err);}}看起来同步,实则异步,心智负担骤降 80%。
但坑也悄悄埋下:
- await 后面跟非 Promise
别笑,真有人这么干:
constnum=await42;// 等价于 Promise.resolve(42)JS 会先把 42 包成 Promise 再拆,完全多此一举,但不会报错,导致后人一脸懵:这里到底是异步还是同步?
- 在 forEach 里 await
经典翻车现场:
constids=[1,2,3];ids.forEach(asyncid=>{constuser=awaitfetchUser(id);console.log(user);});console.log('done');执行顺序先打印done,再乱序打印用户。
因为forEach的回调是同步执行,它才不管你是不是 async,结果就是并发三请求,谁也不等谁。
正确姿势要么用for...of:
for(constidofids){constuser=awaitfetchUser(id);// 顺序执行console.log(user);}要么提前包成数组再Promise.all并发:
awaitPromise.all(ids.map(asyncid=>{constuser=awaitfetchUser(id);console.log(user);}));- 忘了 return Promise
下面这种错误我帮同事排了三次:
asyncfunctionupdateUser(id){awaitfetchUser(id);// 忘记 return,调用方以为没结束}// 外面updateUser(1).then(()=>console.log('更新完'));// 永远不会进回调async 函数不写 return,默认return undefined,调用方then里拿到的永远是undefined,线上连锁反应就是数据没更新完就跳转到下一页,用户骂娘。
Generator 函数:被时代遗忘的中间人
yield这哥们 2015 年伴随 ES6 横空出世,配合 co 库可以手写“半自动 async/await”:
constco=require('co');constfetch=require('node-fetch');function*showUser(userId){constuser=yieldfetch(`/user/${userId}`).then(r=>r.json());constorder=yieldfetch(`/order/${user.lastOrderId}`).then(r=>r.json());returnorder;}co(showUser(1)).then(order=>console.log(order));当年没有原生 async/await,全靠 co 拯救世界。
如今新项目基本绝迹,只有老项目里偶尔能碰到,像翻旧相册看到杀马特照片,满满年代感。
实际开发怎么选?看场景别死磕
- 接口请求:无脑 async/await,代码像同步,心智负担最低。
- 并发上限:接口 QPS 有限制,别一口气
Promise.all两百个请求,后端小哥会提刀。
用信号量库(如p-limit)控制并发:
constpLimit=require('p-limit');constlimit=pLimit(5);// 同时最多 5 个constresults=awaitPromise.all(ids.map(id=>limit(()=>fetchUser(id))));定时器、文件读写、WebSocket:Node 端文件流推荐用原生
fs.promises+ async;
浏览器端定时器还是回调顺手,别硬包 Promise,否则clearTimeout还得再拆一层。大项目统一风格:
团队规约直接写死:- 所有新文件用 async/await;
- 老回调文件加
// @legacy标记,谁动谁重构; - 公用库必须返回 Promise,禁止回调出口。
别让一个仓库里三种写法互踩,Code Review 时你会怀疑人生。
异步代码调试太难?教你几招骚操作
- console.log 顺序乱?
打印前先标记:
console.log('[User] 开始获取',Date.now());awaitfetchUser(id);console.log('[User] 获取完毕',Date.now());Chrome DevTools → 右上角设置 → 勾选 “Async”
开启后 Call Stack 能看到 async 跨越栈,谁调用的 await 一目了然。Node 用 async_hooks
冷门但救命,能追踪同一次请求里所有异步资源,做链路日志神器:
constasyncHooks=require('async_hooks');constfs=require('fs');conststore=newMap();asyncHooks.createHook({init(asyncId,type,triggerAsyncId){store.set(asyncId,store.get(triggerAsyncId)||{id:asyncId});},destroy(asyncId){store.delete(asyncId);}}).enable();// 日志里附带 asyncId,就能串起整条链路那些年我们踩过的异步大坑
- 共享变量翻车
并发请求共用一个let i,结果后到的请求把先到的覆盖,数据全串:
for(vari=0;i<ids.length;i++){fetchUser(ids[i]).then(user=>{console.log('用户',i,user);// 全打印 ids.length});}改用let或闭包保平安:
for(leti=0;i<ids.length;i++){fetchUser(ids[i]).then(user=>{console.log('用户',i,user);});}- Promise 构造函数里抛错
newPromise(()=>{thrownewError('boom');});// 错误静默,外面啥也 catch 不到正确写法永远包reject:
newPromise((resolve,reject)=>{try{thrownewError('boom');}catch(e){reject(e);}});提升可读性的土办法
命名别偷懒
fetchUserByPhone比getData强十倍,后者三个月后你自己都忘了是干啥。拆函数
别把所有 await 堆在一个 async,像下面这种:
asyncfunctionhandleSubmit(){constuser=awaitfetchUser();constorder=awaitfetchOrder();constgoods=awaitfetchGoods();constinvoice=awaitcreateInvoice();constemail=awaitsendEmail();}拆:
asyncfunctionhandleSubmit(){constuser=awaitfetchUser();constorder=awaitfetchOrder(user.id);constgoods=awaitfetchGoods(order.goodsId);awaitsendFlow(order,goods);}asyncfunctionsendFlow(order,goods){constinvoice=awaitcreateInvoice(order);awaitsendEmail(invoice);}逻辑分层,单测也好写。
- 注释标异步入口
在函数头顶加#async标签,Code Review 时一眼识别:
/** * 异步入口:获取用户并发送短信 * @async */exportasyncfunctionuserRegister(){...}异步不是魔法,写清楚比写炫更重要
别为了秀技术硬上 Generator,老板要的是需求按时上线,不是看你表演协程。
代码能跑 ≠ 代码能维护,半夜被叫醒改 bug 的是你自己。
记住一句话:异步写得好,下班跑得早;异步写得乱,通宵到天亮。
今晚就把文章转给队友,别再 forEach 里 await 了,求你们了。
欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!
| 专栏系列(点击解锁) | 学习路线(点击解锁) | 知识定位 |
|---|---|---|
| 《微信小程序相关博客》 | 持续更新中~ | 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等 |
| 《AIGC相关博客》 | 持续更新中~ | AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结 |
| 《HTML网站开发相关》 | 《前端基础入门三大核心之html相关博客》 | 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识 |
| 《前端基础入门三大核心之JS相关博客》 | 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。 通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心 | |
| 《前端基础入门三大核心之CSS相关博客》 | 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页 | |
| 《canvas绘图相关博客》 | Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化 | |
| 《Vue实战相关博客》 | 持续更新中~ | 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅 |
| 《python相关博客》 | 持续更新中~ | Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具 |
| 《sql数据库相关博客》 | 持续更新中~ | SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能 |
| 《算法系列相关博客》 | 持续更新中~ | 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维 |
| 《IT信息技术相关博客》 | 持续更新中~ | 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识 |
| 《信息化人员基础技能知识相关博客》 | 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方 | |
| 《信息化技能面试宝典相关博客》 | 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面 | |
| 《前端开发习惯与小技巧相关博客》 | 持续更新中~ | 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等 |
| 《photoshop相关博客》 | 持续更新中~ | 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结 |
| 日常开发&办公&生产【实用工具】分享相关博客》 | 持续更新中~ | 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具 |
吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!