前端必会:Promise多请求+finally+链式调用避坑指南

前端必会:Promise多请求+finally+链式调用避坑指南
在这里插入图片描述


前端必会:Promise多请求+finally+链式调用避坑指南

前端必会:Promise多请求+finally+链式调用避坑指南

先说点掏心窝子的

当年我被Promise坑到怀疑人生的那些夜晚

说实话,我到现在都记得那个凌晨三点。项目明天上线,我在公司厕所隔间里蹲着改bug,不是因为肚子疼,是因为实在没脸坐在工位上——整个页面的数据全乱套了。

那时候我刚从jQuery转到Vue,满脑子还是$.ajax的回调写法。leader说现在都用Promise了,你改改吧。我想着能有多难?不就是.then()嘛。结果三个接口嵌套调用,我在第三个then里拿不到第一个接口的数据,整个人直接裂开。

那天晚上我对着屏幕发了半小时呆,脑子里只有一个念头:这破玩意儿谁发明的?后来我才明白,Promise这货就像你对象——你说不清她为啥生气,但你得知道怎么哄。不会哄?那就等着凉凉吧。

还有个更惨的兄弟,我前同事。他用Promise.all批量上传图片,结果其中一张上传失败,整个流程直接中断,前面传成功的也白传了。产品经理当场暴走,他在群里发了个"我的锅",然后默默点了外卖准备通宵重写。这种痛,经历过的人才懂。

为啥现在还在讲Promise?这玩意儿过时了吗

我知道你要说啥——“都2024年了,谁还用Promise啊,直接上async/await不香吗?”

香,确实香。但兄弟,你面试的时候面试官问你"Promise原理是什么",你总不能回他"我用async/await所以不用懂Promise"吧?而且你看那些开源库的源码,哪个不是Promise打底?axios、fetch、甚至Vue3的响应式系统,底层全是这玩意儿。

再说了,async/await本质就是Promise的语法糖。糖吃多了不知道糖是怎么做的,万一哪天遇到诡异bug,你连从哪下手都不知道。我见过太多人写await的时候忘了加try-catch,错误直接抛到全局,页面白屏了都不知道咋回事。

还有更现实的:你接手的老项目里全是回调地狱,老板让你重构,你说"我要用async/await所以先把整个项目重写一遍"?怕不是想被优化。渐进式改造,用Promise封装老代码,这才是打工人的生存智慧。

看完这篇你能少加多少班,心里有点数

我不敢说看完你能成为Promise大神,但至少下次遇到这些场景你不会慌:

  • 页面初始化要调五六个接口,怎么保证顺序又不阻塞?
  • 上传文件要显示总进度,单个失败不能影响其他的
  • 接口超时了要自动重试,重试三次还不行再报错
  • 不管成功失败都要关掉loading,这个放哪写?

这些问题我全踩过坑,今天一股脑儿倒给你。代码都是我从项目里扒出来的,改改就能用。省下来的时间,够你多打两把游戏,或者早点下班陪对象——虽然你可能没有对象,但万一有了呢?


Promise这货到底是个啥

别整那些虚的,用大白话讲清楚Promise是干啥的

Promise翻译成中文叫"承诺",但我觉得叫"保证书"更贴切。就像你小时候你妈让你保证"考不到90分就不看电视",这个保证有三种结局:

  1. 兑现了(fulfilled):真考了90分,奖励你吃顿好的
  2. 没兑现(rejected):考了59分,混合双打伺候
  3. 还没考呢(pending):正在考试,结果未知

代码里就是这样:

// 创建一个Promise,相当于写下保证书const myPromise =newPromise((resolve, reject)=>{// 这里写异步操作,比如考试const score = Math.random()>0.5?95:58;if(score >=90){resolve('考了'+ score +'分,妈我厉害吧!');// 兑现承诺}else{reject('只考了'+ score +'分...');// 没兑现}});// 看看结果咋样 myPromise .then(result=>{ console.log(result);// 考好了走这里}).catch(error=>{ console.log(error);// 考砸了走这里});

看到没?resolvereject就是两个回调函数,成功调用resolve,失败调用reject。这俩名字起得挺装,其实就是successfail的文艺版。

三种状态来回切换,比你对象心情还难猜

Promise有三种状态,而且一旦变了就改不回来,这点很重要:

  • Pending(等待中):刚创建的时候,就像你刚发消息给女神,她还没回
  • Fulfilled(已成功):女神回"好的",你心花怒放
  • Rejected(已失败):女神回"滚",你万念俱灰
const promise =newPromise((resolve, reject)=>{ console.log(promise);// 这里打印不出来,因为创建时还没赋值setTimeout(()=>{resolve('成功了');// 注意:这里再reject已经没用了,状态只会变一次reject('失败了');// 这行不会执行,或者执行了也无效},1000);});// 状态一旦确定,再操作也没用 promise.then(console.log).catch(console.error);

为啥说比你对象心情还难猜?因为对象心情还能哄好,Promise状态变了就是变了,天王老子来了也改不了。所以写代码的时候千万注意,别在resolve后面又写个reject,看着挺严谨,实际全是废话。

还有个坑:如果你创建Promise的时候忘了调用resolvereject,那它就会一直pending。就像你给女神发消息,她已读不回,你永远不知道她在想啥。这种"内存泄漏"式bug最难查,因为控制台也不报错,就是没反应。

// 错误示范:永远pending的Promiseconst badPromise =newPromise((resolve, reject)=>{ console.log('我开始执行了');// 完了,忘了resolve了!}); badPromise.then(()=>{ console.log('这行永远不会执行');});

怎么发现这种问题?浏览器开发者工具的Memory面板可以拍快照,看看有没有一直pending的Promise对象。或者简单点,给Promise加个超时逻辑:

// 给Promise加超时,防止一直pendingfunctionwithTimeout(promise, ms){const timeout =newPromise((_, reject)=>{setTimeout(()=>reject(newError('超时了兄弟!')), ms);});return Promise.race([promise, timeout]);}// 使用withTimeout(badPromise,5000).catch(err=> console.log(err.message));

从回调地狱到链式调用,前端人的血泪进化史

没Promise之前,我们写异步代码是这样的:

// 回调地狱,也叫"末日金字塔"getUserId(function(userId){getUserInfo(userId,function(userInfo){getOrders(userInfo.id,function(orders){getOrderDetails(orders[0].id,function(details){getProductInfo(details.productId,function(product){ console.log('终于拿到了:', product);// 到这里已经缩进5层了,屏幕都快不够宽了},function(err){ console.error('获取商品信息失败', err);});},function(err){ console.error('获取订单详情失败', err);});},function(err){ console.error('获取订单列表失败', err);});},function(err){ console.error('获取用户信息失败', err);});},function(err){ console.error('获取用户ID失败', err);});

这代码看着像不像金字塔?而且每个错误处理都得单独写,写到最后你都不知道自己在处理哪层的错误。我当年维护过一个老项目,回调嵌了8层,改个bug要捋半小时逻辑,改完还得祈祷别影响其他层。

有了Promise之后,世界清爽多了:

// 链式调用,像串糖葫芦一样清爽getUserId().then(userId=>getUserInfo(userId)).then(userInfo=>getOrders(userInfo.id)).then(orders=>getOrderDetails(orders[0].id)).then(details=>getProductInfo(details.productId)).then(product=>{ console.log('拿到了:', product);}).catch(err=>{ console.error(' somewhere出错了:', err);// 一个catch管所有});

看到没?错误处理统一了,缩进也正常了。而且then里面可以return一个新的Promise,继续往下链。这种写法不仅好看,还好调试——你在任意一个then里打断点,都能看到前面传过来的数据。

但链式调用也有坑,后面会细说。先记住一点:每个then都要记得return,不然数据就断了


多个请求一起搞,姿势要对

实际开发中,很少有一次只发一个请求的情况。页面一打开,用户信息、轮播图、商品列表、未读消息…恨不得一次性全拉回来。这时候怎么安排这些请求,直接决定你的页面加载速度。

Promise.all一把梭,全成功才返回,失败直接凉凉

这是最常用的,也是坑最多的。Promise.all接收一个Promise数组,全部成功才返回结果数组,任意一个失败就直接进catch。

// 同时发起三个请求const p1 =fetch('/api/user');const p2 =fetch('/api/orders');const p3 =fetch('/api/messages'); Promise.all([p1, p2, p3]).then(([res1, res2, res3])=>{// 三个都成功了,这里才能拿到数据return Promise.all([res1.json(), res2.json(), res3.json()]);}).then(([user, orders, messages])=>{ console.log('用户信息:', user); console.log('订单列表:', orders); console.log('消息列表:', messages);}).catch(err=>{// 任意一个失败了,直接走这里 console.error('至少有一个接口挂了:', err);// 注意:这时候你不知道是哪个挂了,得看err.message猜});

看起来挺美,但实际项目里这么用,分分钟翻车。比如用户订单接口挂了,但用户信息和消息是好的,这时候你直接显示"加载失败",用户体验极差。更惨的是,如果三个接口里有一个特别慢,用户得等最慢的那个完事才能看到页面。

所以Promise.all只适合强依赖场景——比如必须同时拿到用户信息和权限列表,才能决定显示什么页面。但凡有一个失败,页面确实没法用,这时候失败就失败吧。

还有个细节:返回的结果数组顺序和传入的Promise数组顺序一致,不是谁先返回谁在前面。这点很重要,别搞混了。

// 顺序是固定的,和请求快慢无关const slow =newPromise(resolve=>setTimeout(()=>resolve('慢'),1000));const fast =newPromise(resolve=>setTimeout(()=>resolve('快'),100)); Promise.all([slow, fast]).then(results=>{ console.log(results);// ['慢', '快'],不是['快', '慢']});

Promise.allSettled才是亲儿子,不管成败都能拿到结果

ES2020新增的API,这才是处理多请求的正经姿势。它不管成功失败,等所有Promise都" settled"(尘埃落定)之后,返回一个数组,告诉你每个的结果。

const promises =[fetch('/api/user').then(r=> r.json()),fetch('/api/orders').then(r=> r.json()),// 假设这个会失败fetch('/api/messages').then(r=> r.json())]; Promise.allSettled(promises).then(results=>{ results.forEach((result, index)=>{if(result.status ==='fulfilled'){ console.log(`第${index}个请求成功:`, result.value);}else{ console.error(`第${index}个请求失败:`, result.reason);}});// 实际项目中,你可以这样处理:const user = results[0].status ==='fulfilled'? results[0].value :null;const orders = results[1].status ==='fulfilled'? results[1].value :[];const messages = results[2].status ==='fulfilled'? results[2].value :[];// 渲染页面,失败的用默认值兜底renderPage({ user, orders, messages });});

看到没?即使中间那个接口挂了,其他的数据照样能拿到。返回的数组里,每个元素是个对象,有status字段标记状态,value存成功结果,reason存失败原因。

实际项目里,我基本都是用allSettled,除非业务上确实要求"一个失败全失败"。比如支付流程,扣款和生成订单必须都成功,这时候才用all

Promise.race玩的就是心跳,谁快听谁的

字面意思:赛跑,谁第一个 settled(不管是成功还是失败),就听谁的。

// 场景1:请求超时控制constfetchWithTimeout=(url, ms =5000)=>{const fetchPromise =fetch(url);const timeoutPromise =newPromise((_, reject)=>{setTimeout(()=>reject(newError('请求超时')), ms);});return Promise.race([fetchPromise, timeoutPromise]);};// 使用fetchWithTimeout('/api/slow-data',3000).then(response=> response.json()).then(data=> console.log('拿到了:', data)).catch(err=> console.error(err.message));// 如果超过3秒没返回,这里会报"请求超时"// 场景2:多个CDN地址,谁快用谁constloadScript=(src)=>{returnnewPromise((resolve, reject)=>{const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script);});};const cdn1 ='https://cdn1.example.com/lib.js';const cdn2 ='https://cdn2.example.com/lib.js';const cdn3 ='https://cdn3.example.com/lib.js'; Promise.race([loadScript(cdn1),loadScript(cdn2),loadScript(cdn3)]).then(()=> console.log('有个CDN加载成功了')).catch(()=> console.error('所有CDN都挂了'));

注意:race只要有一个settled就结束,其他的不管了。所以如果第一个失败,就算后面有成功的,你也拿不到。这点和any不一样。

Promise.any新出的狠角色,只要有一个成功就行

ES2021新增的,和race类似,但它只关心第一个成功的,如果全都失败,才报错。

const promises =[fetch('https://unreliable-api-1.com/data'),// 可能失败fetch('https://unreliable-api-2.com/data'),// 可能失败 fetch('https://reliable-api.com/data')// 这个能成功]; Promise.any(promises).then(response=> response.json()).then(data=>{ console.log('至少有一个成功了:', data);// 这里拿到的是第一个成功的那个的结果}).catch(err=>{// 所有都失败了才会进这里 console.error('全挂了:', err);// err是一个AggregateError,包含所有失败原因 err.errors.forEach((e, i)=>{ console.error(`第${i}个失败原因:`, e.message);});});

这个在"多源备份"场景特别好用。比如你有三个图片CDN,只要有一个能加载出来就行,不用管其他的。比race更智能,因为race如果第一个CDN返回404(失败了),就直接进catch了,哪怕第二个CDN其实是好的。

但注意浏览器兼容性,IE肯定不支持,老旧安卓机可能也有问题。生产环境用之前先检查下caniuse,或者上polyfill。

实际项目中咋选,看场景别瞎用

给你个速查表,收藏备用:

方法成功条件失败条件适用场景
Promise.all全部成功任意一个失败强依赖,如支付流程
Promise.allSettled全部完成(不论成败)不会失败弱依赖,如首页多模块数据
Promise.race第一个settled第一个settled超时控制、多源竞速
Promise.any第一个成功全部失败多源备份、容错加载

代码示例,一个真实的页面初始化场景:

// 首页初始化,分三类请求asyncfunctioninitHomePage(){// 1. 关键数据,必须全拿到,否则页面没法用const criticalData =await Promise.all([fetchUserInfo(),fetchPermissions()]).catch(err=>{// 这里失败了直接跳错误页showErrorPage('系统初始化失败,请刷新重试');throw err;// 中断后续执行});// 2. 次要数据,能拿到最好,拿不到用默认值const secondaryData =await Promise.allSettled([fetchNotifications(),fetchRecommendations(),fetchAds()]);const notifications = secondaryData[0].status ==='fulfilled'? secondaryData[0].value :[];const recommendations = secondaryData[1].status ==='fulfilled'? secondaryData[1].value :[];// 3. 非关键资源,谁快用谁(比如统计脚本) Promise.race([loadStatsScriptFromCDN1(),loadStatsScriptFromCDN2()]).catch(()=>{// 统计脚本失败不影响业务,静默处理 console.warn('统计脚本加载失败');});// 渲染页面render(criticalData,{ notifications, recommendations });}

finally这块儿真容易翻车

finally是ES2018加入的,意思是"不管成功失败,最后都得干的事"。听起来简单,但坑不少。

finally不管成功失败都会执行,这点得刻在脑子里

constdoSomething=()=>{returnfetch('/api/data').then(res=> res.json()).then(data=>{ console.log('成功拿到数据');return data;}).catch(err=>{ console.error('出错了:', err);throw err;// 继续抛出,让上层处理}).finally(()=>{ console.log('清理工作:关闭loading、重置状态');// 这里一定会执行,不管上面是then还是catch});};// 测试成功场景doSomething().then(console.log).catch(console.error);// 输出:成功拿到数据 -> 清理工作 -> [数据]// 测试失败场景(把接口地址改错)// 输出:出错了:... -> 清理工作 -> [错误]

实际项目中最常见的用法就是关闭loading:

classApiService{asyncrequest(url, options ={}){this.showLoading();// 显示loadingtry{const response =awaitfetch(url, options);if(!response.ok)thrownewError('HTTP '+ response.status);returnawait response.json();}catch(error){this.handleError(error);throw error;// 继续抛出,让业务层知道}finally{this.hideLoading();// 无论如何都要关掉loading,不然页面就卡死了}}showLoading(){ document.getElementById('loading').style.display ='block';}hideLoading(){ document.getElementById('loading').style.display ='none';}handleError(error){ console.error('API错误:', error);// 可以在这里统一弹toast}}

为啥finally里不能改数据,踩过的坑说出来都是泪

finally里可以写代码,但它的返回值不会影响整个Promise的结果。也就是说,你在finally里return啥都没用,该返回啥还是返回啥。

Promise.resolve('原始数据').then(data=>{ console.log('then:', data);// then: 原始数据return data +'-处理后';}).finally(()=>{ console.log('finally执行了');return'finally的数据';// 这行没用!}).then(result=>{ console.log('最终结果:', result);// 最终结果: 原始数据-处理后});

看到没?finally里return的字符串被忽略了,最终结果还是then里return的。那finally到底能干啥?只能做副作用操作(side effects),比如清理资源、记录日志、改变UI状态,不能修改数据流。

我踩过的坑:曾经在finally里想给数据加个标记,结果下游一直拿不到,debug了两小时才发现是finally的返回值被吞了。正确做法是在then里处理好再往下传。

// 错误示范fetchData().then(data=>{// 处理数据returnprocess(data);}).finally(processedData=>{// 注意:finally回调没有参数!// 想在这里再加个标记,结果processedData是undefinedreturn{...processedData,flag:true};// 完全没用});// 正确做法fetchData().then(data=>{const processed =process(data);return{...processed,flag:true};// 在then里处理好}).finally(()=>{// 这里只干清理工作cleanup();});

关闭loading、清理定时器这些活儿交给它最合适

除了loading,这些场景也适合放finally

// 场景1:清理定时器functionfetchWithTimeout(url, ms){let timer;return Promise.race([fetch(url),newPromise((_, reject)=>{ timer =setTimeout(()=>reject(newError('超时')), ms);})]).finally(()=>{clearTimeout(timer);// 不管成功失败,定时器都得清,不然内存泄漏});}// 场景2:释放锁(比如防止重复提交)classSubmitController{constructor(){this.isSubmitting =false;}asyncsubmit(data){if(this.isSubmitting)return;this.isSubmitting =true;// 加锁try{const result =await api.submit(data);showSuccess('提交成功');return result;}catch(error){showError('提交失败:'+ error.message);throw error;}finally{this.isSubmitting =false;// 无论如何都要解锁,不然下次点不了了}}}// 场景3:恢复按钮状态asyncfunctionhandleClick(){const btn = document.getElementById('submitBtn'); btn.disabled =true; btn.textContent ='提交中...';try{awaitsubmitForm();}finally{// 不管成功失败,按钮都得恢复 btn.disabled =false; btn.textContent ='提交';}}

finally返回的值会不会影响链式,实测给你看

前面说了finally的return没用,但如果在finally抛出错误,那就会改变Promise的状态:

Promise.resolve('一切正常').finally(()=>{ console.log('finally执行');thrownewError('finally里出错了!');// 抛出错误}).then(result=>{ console.log('成功:', result);// 不会执行}).catch(err=>{ console.error('捕获到:', err.message);// 捕获到: finally里出错了!});

看到没?本来 resolved 的Promise,因为finally里抛了错,变成了 rejected。这点要特别注意,finally里尽量别抛错,除非你真的想中断流程。

还有更隐蔽的坑:finally里如果返回一个 rejected 的Promise,效果等同于抛错:

Promise.resolve('正常').finally(()=>{return Promise.reject('finally返回的reject');// 这也会让整体变reject}).catch(err=> console.error(err));// 输出: finally返回的reject

所以finally的最佳实践是:只做同步的清理工作,别搞异步操作,更别抛错


链式调用写爽了是真的爽

Promise的精髓就是链式调用,.then().then().then()看着就舒服。但写爽了容易飘,一飘就翻车。

then接then像串糖葫芦,数据一层层往下传

每个then都可以接收上一个then的返回值:

getUserId().then(userId=>{ console.log('拿到userId:', userId);// 123returngetUserInfo(userId);// 返回新的Promise}).then(userInfo=>{ console.log('拿到userInfo:', userInfo);// {name: '张三', id: 123}returngetOrders(userInfo.id);// 继续返回}).then(orders=>{ console.log('拿到orders:', orders);// [{id: 1}, {id: 2}]// 不想继续了,返回普通值也行return orders.length;}).then(count=>{ console.log('订单数量:', count);// 2});

关键点:一定要return! 如果不return,下一个then拿到的是undefined

getUserId().then(userId=>{getUserInfo(userId);// 忘了return!}).then(userInfo=>{ console.log(userInfo);// undefined,因为上一个then没return});

这种bug最难查,因为控制台不报错,只是数据不对。建议写then的时候先写return,再填内容。

catch放哪有讲究,放错了bug找到你头秃

catch能捕获它前面所有then的错误,所以位置很重要:

// 方案1:catch放在最后,捕获所有错误fetchUser().then(user=>fetchOrders(user.id)).then(orders=>fetchDetails(orders[0].id)).then(details=>render(details)).catch(err=>{// 上面任意一步出错都会到这里 console.error(' somewhere错了:', err);});// 方案2:catch放在中间,只处理前面的,后面的继续执行fetchUser().then(user=>fetchOrders(user.id)).catch(err=>{// 只处理fetchUser和fetchOrders的错误 console.error('获取用户或订单失败:', err);return[];// 返回默认值,让链式继续}).then(orders=>{// 即使前面出错了,这里也会执行,orders是[]returnfetchDetails(orders[0]?.id);}).then(details=>render(details)).catch(err=>{// 处理fetchDetails和render的错误 console.error('渲染失败:', err);});

实际项目中,我经常用这种"中间catch"做容错:

// 加载页面数据,非关键模块失败不影响其他模块 Promise.all([fetchCriticalData().catch(err=>{// 关键数据失败,直接抛,页面没法用了throw err;}),fetchModuleA().catch(err=>{ console.warn('模块A加载失败:', err);returnnull;// 返回null,页面继续渲染}),fetchModuleB().catch(err=>{ console.warn('模块B加载失败:', err);returnnull;})]).then(([critical, moduleA, moduleB])=>{renderPage({ critical, moduleA, moduleB });});

链子太长怎么办,async/await来救场

虽然链式调用很爽,但超过3个then就有点难读了。这时候可以用async/await重构:

// 链式写法,有点长了fetchUser().then(user=>fetchOrders(user.id)).then(orders=> Promise.all(orders.map(o=>fetchDetail(o.id)))).then(details=> details.filter(d=> d.status ==='active')).then(activeItems=>render(activeItems)).catch(err=> console.error(err));// async/await写法,更像同步代码asyncfunctionloadAndRender(){try{const user =awaitfetchUser();const orders =awaitfetchOrders(user.id);const details =await Promise.all(orders.map(o=>fetchDetail(o.id)));const activeItems = details.filter(d=> d.status ==='active');render(activeItems);}catch(err){ console.error(err);}}

但注意,async/await不是万能的。比如你想并行执行请求,还是得用Promise.all

// 错误:这样是串行的,慢!const user =awaitfetchUser();const orders =awaitfetchOrders(user.id);// 等user回来才发请求const messages =awaitfetchMessages(user.id);// 等orders回来才发请求// 正确:并行执行orders和messagesconst user =awaitfetchUser();const[orders, messages]=await Promise.all([fetchOrders(user.id),fetchMessages(user.id)]);

别再then里面嵌套then了,求你了

我见过最恐怖的代码,then里套then,套了三四层:

// 末日嵌套,千万别这么写!fetchUser().then(user=>{fetchOrders(user.id).then(orders=>{fetchDetails(orders[0].id).then(details=>{ console.log(details);});});});

这种写法失去了Promise的意义,又回到了回调地狱。要么把嵌套拆成链式,要么用async/await:

// 拆成链式fetchUser().then(user=>fetchOrders(user.id)).then(orders=>fetchDetails(orders[0].id)).then(details=> console.log(details));// 或者用async/awaitconst user =awaitfetchUser();const orders =awaitfetchOrders(user.id);const details =awaitfetchDetails(orders[0].id); console.log(details);

这玩意儿好在哪,坑又在哪

代码看着清爽多了,回调地狱终于能告别

这点前面说了很多,不再啰嗦。总之就是:从横向缩进地狱变成纵向链式调用,可读性提升10倍。

错误处理统一了,不用到处try-catch

传统回调每个异步操作都要写错误处理,Promise可以在最后统一catch。而且Promise的错误会冒泡,直到被捕获,不会静默失败(除非你真的忘了写catch)。

// 传统回调,错误处理分散step1((err, res1)=>{if(err){handle(err);return;}step2(res1,(err, res2)=>{if(err){handle(err);return;}step3(res2,(err, res3)=>{if(err){handle(err);return;}// ...});});});// Promise,错误统一处理step1().then(res1=>step2(res1)).then(res2=>step3(res2))// ... 中间不用管错误.catch(err=>handle(err));// 最后统一处理

学习曲线有点陡,刚开始真的容易懵

Promise的概念不难,但细节很多:什么时候return、catch放哪、finally能不能改数据…这些坑踩过才知道。而且错误信息有时候很迷,比如"Uncaught (in promise) Error",新手完全不知道哪漏了catch。

建议学习路径:

  1. 先理解三种状态和基本用法
  2. 练习链式调用,重点练"return"
  3. 学多请求处理(all/race等)
  4. 最后学async/await,对比着学

老项目改造成本高,别指望一天搞定

如果你接手的是jQuery项目,全是$.ajax回调,别想着一次性全改成Promise。渐进式改造:

// 第一步:封装现有回调为PromisefunctionpromisifyAjax(url, options){returnnewPromise((resolve, reject)=>{ $.ajax({ url,...options,success: resolve,error:(xhr, status, err)=>reject(err)});});}// 第二步:新代码用Promise写法promisifyAjax('/api/data').then(data=> console.log(data));// 第三步:旧代码逐步替换,别一次性全改,容易出大事

实际干活时咋用

接口批量请求,用户信息+订单数据一起拉

// 页面初始化,拉取核心数据asyncfunctioninitDashboard(){const loading =showLoading();try{// 并行请求,但做容错处理const[userResult, ordersResult, statsResult]=await Promise.allSettled([fetchUser(),fetchOrders(),fetchStats()]);// 处理用户数据(关键,失败则整页报错)if(userResult.status ==='rejected'){thrownewError('获取用户信息失败:'+ userResult.reason);}const user = userResult.value;// 处理订单数据(非关键,失败用空数组)const orders = ordersResult.status ==='fulfilled'? ordersResult.value :[];if(ordersResult.status ==='rejected'){ console.warn('订单数据获取失败:', ordersResult.reason);}// 处理统计数据(非关键)const stats = statsResult.status ==='fulfilled'? statsResult.value :{total:0,count:0};// 渲染页面renderDashboard({ user, orders, stats });}finally{hideLoading(loading);}}

上传多个文件,进度条怎么搞

classFileUploader{constructor(files){this.files = files;this.total = files.length;this.completed =0;this.failed =0;}asyncuploadAll(){const uploadPromises =this.files.map(file=>this.uploadSingle(file));// 用allSettled,不管单个成功失败,都要更新进度const results =await Promise.allSettled(uploadPromises);return{success: results.filter(r=> r.status ==='fulfilled').map(r=> r.value),failed: results.filter(r=> r.status ==='rejected').map(r=> r.reason)};}uploadSingle(file){returnnewPromise((resolve, reject)=>{const xhr =newXMLHttpRequest(); xhr.upload.addEventListener('progress',(e)=>{if(e.lengthComputable){const percent =(e.loaded / e.total)*100;this.updateProgress(file.name, percent);}}); xhr.addEventListener('load',()=>{if(xhr.status ===200){this.completed++;this.updateTotalProgress();resolve({file: file.name,response: xhr.response });}else{reject(newError(`${file.name} 上传失败: ${xhr.status}`));}}); xhr.addEventListener('error',()=>{this.failed++;this.updateTotalProgress();reject(newError(`${file.name} 网络错误`));}); xhr.open('POST','/api/upload'); xhr.send(file);});}updateProgress(filename, percent){ console.log(`${filename}: ${percent.toFixed(1)}%`);// 实际项目中这里更新DOM}updateTotalProgress(){const totalPercent =((this.completed +this.failed)/this.total)*100; console.log(`总进度: ${totalPercent.toFixed(1)}% (${this.completed}成功, ${this.failed}失败)`);}}// 使用const files = document.getElementById('fileInput').files;const uploader =newFileUploader(Array.from(files)); uploader.uploadAll().then(result=>{ console.log('上传完成:', result);});

请求超时+重试机制,finally在这里派上用场

asyncfunctionfetchWithRetry(url, options ={}){const{ maxRetries =3, timeout =5000, retryDelay =1000}= options;let lastError;for(let attempt =1; attempt <= maxRetries; attempt++){ console.log(`第${attempt}次尝试...`);try{// 使用Promise.race实现超时const controller =newAbortController();const timeoutId =setTimeout(()=> controller.abort(), timeout);try{const response =awaitfetch(url,{...options,signal: controller.signal });if(!response.ok){thrownewError(`HTTP ${response.status}`);}returnawait response.json();}finally{// 无论如何都要清定时器,避免内存泄漏clearTimeout(timeoutId);}}catch(error){ lastError = error; console.warn(`第${attempt}次失败:`, error.message);if(attempt < maxRetries){// 等待一段时间再重试awaitnewPromise(resolve=>setTimeout(resolve, retryDelay));}}}thrownewError(`重试${maxRetries}次后仍然失败: ${lastError.message}`);}// 使用fetchWithRetry('/api/unstable-endpoint',{maxRetries:3,timeout:3000,retryDelay:500}).then(data=> console.log('成功:', data)).catch(err=> console.error('彻底失败:', err));

登录鉴权流程,链式调用把逻辑串起来

classAuthService{asynclogin(credentials){returnfetch('/api/login',{method:'POST',body:JSON.stringify(credentials)}).then(res=>{if(!res.ok)thrownewError('登录失败');return res.json();}).then(data=>{// 保存token localStorage.setItem('token', data.token);return data.user;}).then(user=>{// 获取用户权限returnthis.fetchPermissions(user.id).then(permissions=>({...user, permissions }));}).then(userWithPerms=>{// 获取未读消息returnthis.fetchUnreadCount().then(count=>({...userWithPerms,unreadCount: count }));}).then(finalUser=>{// 更新全局状态this.updateGlobalState(finalUser);return finalUser;}).catch(err=>{// 清理可能残留的token localStorage.removeItem('token');throw err;});}asyncfetchPermissions(userId){const res =awaitfetch(`/api/users/${userId}/permissions`);return res.json();}asyncfetchUnreadCount(){const res =awaitfetch('/api/messages/unread');const data =await res.json();return data.count;}updateGlobalState(user){ window.$store?.commit('setUser', user);}}// 使用const auth =newAuthService(); auth.login({username:'admin',password:'123456'}).then(user=>{ console.log('登录成功:', user); router.push('/dashboard');}).catch(err=>{alert(err.message);});

出问题了咋排查

Promisepending了不往下走,大概率是忘了resolve

症状:代码执行了,但then里的逻辑没触发,也不报错。

检查点:

  1. 是否忘了调用resolve/reject
  2. 是否把resolve写成了reslove(拼写错误)
  3. 是否在new Promise里抛了同步错误(这种会直接reject,但不会走then
// 错误:忘了resolvenewPromise((resolve)=>{setTimeout(()=>{ console.log('时间到了');// 忘了调用resolve!},1000);}).then(()=> console.log('这行不会执行'));// 错误:拼写错误newPromise((resolve)=>{reslove('数据');// 拼写错误,resolve没调用}).then(console.log);// 错误:同步抛错newPromise(()=>{thrownewError('同步错误');}).catch(err=> console.log('会进这里:', err));

错误吞掉了没报错,检查catch有没有漏

Promise的错误如果没有被catch,会报"Uncaught (in promise)",但有时候你明明写了catch,错误还是没了,检查:

  1. catch里是不是忘了继续抛出错误?
  2. then里是不是返回了undefined,导致后续逻辑没数据?
// 错误:catch吞掉了错误fetchData().then(data=>process(data)).catch(err=>{ console.error(err);// 忘了throw err,后面以为成功了}).then(result=>{// 如果前面出错了,result是undefined,但这里会继续执行saveToDatabase(result);// 可能保存了undefined!});// 正确:需要继续抛的错误要throwfetchData().then(data=>process(data)).catch(err=>{ console.error(err);throw err;// 继续抛出,中断流程}).then(result=>{saveToDatabase(result);});

多个请求顺序乱了,看看是不是该用all还是race

如果请求A依赖请求B的数据,千万别用Promise.all并行执行,要串行:

// 错误:并行执行,userId还没拿到就发第二个请求 Promise.all([fetchUser(),// 返回 {id: 123}fetchOrders(userId)// userId从哪来?undefined!]);// 正确:串行执行fetchUser().then(user=>fetchOrders(user.id));// 或者async/awaitconst user =awaitfetchUser();const orders =awaitfetchOrders(user.id);

浏览器控制台怎么调试Promise,几个小技巧

  1. 在then里打断点:直接在代码里写debugger,或者Chrome开发者工具里点行号
  2. 查看Promise状态:在Console面板输入Promise实例,可以看到[[PromiseState]][[PromiseResult]]
  3. 用console.log追踪:在每个then开头打印数据,看流向
  4. Network面板:确认请求是否真的发出去了,状态码对不对
// 调试技巧:给Promise加日志constdebugPromise=(promise, name)=>{return promise .then(value=>{ console.log(`[${name}] 成功:`, value);return value;}).catch(err=>{ console.error(`[${name}] 失败:`, err);throw err;});};// 使用debugPromise(fetchUser(),'获取用户').then(user=>debugPromise(fetchOrders(user.id),'获取订单'));

async/await混用时报错信息看不懂,教你怎么定位

async/await本质还是Promise,错误栈可能很长。看错误信息时:

  1. 找"Caused by"或"at async"这样的关键字
  2. 从下往上读调用栈,找到你写的代码文件
  3. 如果错误被吞了,在 suspected 的函数外加try-catch
// 错误栈看不懂时,加try-catch定位asyncfunctioncomplexOperation(){try{const a =awaitstep1();const b =awaitstep2(a);const c =awaitstep3(b);return c;}catch(err){ console.error('complexOperation失败:', err); console.error('发生在:', err.stack);// 打印完整堆栈throw err;}}

老手都不一定知道的骚操作

用Promise封装定时器,setInterval也能链式

// 延迟执行constdelay=(ms)=>newPromise(resolve=>setTimeout(resolve, ms));// 使用asyncfunctiondemo(){ console.log('开始');awaitdelay(1000); console.log('1秒后');awaitdelay(1000); console.log('又1秒后');}// 轮询直到条件满足asyncfunctionpollUntil(conditionFn, interval =1000, maxAttempts =10){for(let i =0; i < maxAttempts; i++){if(conditionFn()){returntrue;}awaitdelay(interval);}thrownewError('轮询超时');}// 使用:等待某个元素出现awaitpollUntil(()=> document.getElementById('app')!==null); console.log('app元素出现了');

请求队列控制并发数,别把服务器打崩了

classPromiseQueue{constructor(concurrency =3){this.concurrency = concurrency;this.running =0;this.queue =[];}add(promiseFactory){returnnewPromise((resolve, reject)=>{this.queue.push({ promiseFactory, resolve, reject });this.process();});}process(){if(this.running >=this.concurrency ||this.queue.length ===0){return;}this.running++;const{ promiseFactory, resolve, reject }=this.queue.shift();promiseFactory().then(resolve, reject).finally(()=>{this.running--;this.process();// 处理下一个});}}// 使用:同时最多3个请求const queue =newPromiseQueue(3);// 假设有100个文件要上传const files = Array.from({length:100},(_, i)=>`file${i}.txt`);const uploadPromises = files.map(file=> queue.add(()=>uploadFile(file))// 返回Promise);await Promise.all(uploadPromises); console.log('全部上传完成');

自己手写一个简易Promise,理解更透彻

classMyPromise{constructor(executor){this.state ='pending';this.value =undefined;this.reason =undefined;this.onFulfilledCallbacks =[];this.onRejectedCallbacks =[];constresolve=(value)=>{if(this.state ==='pending'){this.state ='fulfilled';this.value = value;this.onFulfilledCallbacks.forEach(fn=>fn());}};constreject=(reason)=>{if(this.state ==='pending'){this.state ='rejected';this.reason = reason;this.onRejectedCallbacks.forEach(fn=>fn());}};try{executor(resolve, reject);}catch(err){reject(err);}}then(onFulfilled, onRejected){returnnewMyPromise((resolve, reject)=>{if(this.state ==='fulfilled'){setTimeout(()=>{try{const x =onFulfilled(this.value);resolve(x);}catch(err){reject(err);}});}elseif(this.state ==='rejected'){setTimeout(()=>{try{const x =onRejected(this.reason);resolve(x);}catch(err){reject(err);}});}else{// pending状态,先把回调存起来this.onFulfilledCallbacks.push(()=>{setTimeout(()=>{try{const x =onFulfilled(this.value);resolve(x);}catch(err){reject(err);}});});this.onRejectedCallbacks.push(()=>{setTimeout(()=>{try{const x =onRejected(this.reason);resolve(x);}catch(err){reject(err);}});});}});}catch(onRejected){returnthis.then(null, onRejected);}}// 测试newMyPromise((resolve)=>{setTimeout(()=>resolve('成功'),100);}).then(value=>{ console.log('MyPromise:', value);return'链式调用';}).then(value=>{ console.log(value);});

把回调风格的老代码改成Promise,渐进式改造

// 老代码:Node.js风格的回调const fs =require('fs');functionreadFileCallback(path, callback){ fs.readFile(path,'utf8',(err, data)=>{if(err){callback(err);return;}callback(null, data);});}// 改造:包装为PromisefunctionreadFilePromise(path){returnnewPromise((resolve, reject)=>{readFileCallback(path,(err, data)=>{if(err)reject(err);elseresolve(data);});});}// 更通用的promisify工具functionpromisify(fn){returnfunction(...args){returnnewPromise((resolve, reject)=>{fn(...args,(err, result)=>{if(err)reject(err);elseresolve(result);});});};}// 使用const readFile =promisify(fs.readFile);const content =awaitreadFile('./file.txt','utf8');

最后唠两句

写这篇的时候,我又想起那个凌晨三点的厕所隔间。那时候我觉得Promise是世界上最难的东西,现在回头看,其实就那么几个概念:状态、then、catch、finally,再加上all/race这些工具方法。

但编程就是这样,难的不是概念,是细节。哪个then忘了return、catch放错位置、finally里想改数据…这些坑不踩一遍,看十遍文档也记不住。所以我把这些血泪史都写出来了,能帮你少熬几个夜,这篇就没白写。

代码我都跑过,但环境不同可能会有差异。如果你复制粘贴发现有问题,先检查浏览器版本(特别是用any/allSettled这些新API的时候),再看看网络请求是不是真的通了。别一上来就怀疑我代码错了——虽然也有可能,但概率不大。

收藏不收藏随你,反正我写完了。我要下班了,今天不加班,回去煮个泡面加俩蛋,美滋滋。你们随意,有问题下次聊。

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐: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等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!
在这里插入图片描述

Read more

一句话生成PCB?和AI聊聊天,就把板子画了!

一句话生成PCB?和AI聊聊天,就把板子画了!

在键盘上敲下一句“我要一个STM32的电机驱动板,带CAN总线”,几秒后,一张完整的原理图和PCB布局在你眼前展开——这不是科幻电影,而是AI给硬件工程师带来的真实震撼。 清晨的阳光洒进办公室,资深硬件工程师李工没有像往常一样直接打开Altium Designer。他对着电脑屏幕上的对话框,敲入了一行简单的需求描述:“设计一个基于ESP32的智能插座PCB,要求支持Wi-Fi控制、过载保护,尺寸尽量小巧。” 15分钟后,一份完整的原理图草案、经过初步优化的双层板布局,甚至是一份物料清单(BOM)初稿已经呈现在他面前。这不可思议的效率背后,正是AI驱动的PCB设计工具在重新定义电子设计的边界。 01 效率革命,从对话到电路板 如今的PCB设计领域正经历着一场静悄悄的革命。传统上,一块电路板从概念到图纸,需要工程师经历需求分析、器件选型、原理图绘制、布局布线等一系列复杂工序,耗时数天甚至数周。 AI工具的出现彻底改变了这一流程。这类工具的核心是经过海量电路数据和设计规则训练的大型语言模型,它们能理解自然语言描述的需求,自动完成从逻辑设计到物理实现的全流程或关键环节。 比如,当

AI 的智能体专栏:手把手教你用豆包打造专属 Python 智能管家,轻松解决编程难题

AI 的智能体专栏:手把手教你用豆包打造专属 Python 智能管家,轻松解决编程难题

AI 的智能体专栏:手把手教你用豆包打造专属 Python 智能管家,轻松解决编程难题 AI 的智能体专栏:手把手教你用豆包打造专属 Python 智能管家,轻松解决编程难题,本文介绍了如何利用豆包平台打造专属Python智能管家。首先简述豆包平台的核心优势,接着说明创建前的准备工作,包括注册账号、明确定位和收集训练资料。随后详细讲解创建流程,从新建智能体、基础设置、能力配置到测试优化,还提及集成代码执行环境等高级功能扩展,以及使用技巧与实际应用案例。该智能官能解决多种Python编程问题,可提升学习效率和问题解决速度,是实用的个性化编程助手。 前言     人工智能学习合集专栏是 AI 学习者的实用工具。它像一个全面的 AI 知识库,把提示词设计、AI 创作、智能绘图等多个细分领域的知识整合起来。无论你是刚接触 AI 的新手,还是有一定基础想提升的人,都能在这里找到合适的内容。从最基础的工具操作方法,到背后深层的技术原理,专栏都有讲解,还搭配了实例教程和实战案例。这些内容能帮助学习者一步步搭建完整的 AI 知识体系,让大家快速从入门进步到精通,

数智驱动:医学编程与建模技术在智慧医院AI建设中的创新与变革

数智驱动:医学编程与建模技术在智慧医院AI建设中的创新与变革

一、引言 1.1 研究背景与意义 在信息技术飞速发展的数智化时代,医疗行业正经历着深刻变革,医院的发展模式也在不断转型升级。随着人口老龄化加剧、疾病谱的变化以及人们对医疗服务质量要求的日益提高,传统的医疗模式已难以满足社会的需求,智慧医院建设成为医疗行业发展的必然趋势。智慧医院旨在利用先进的信息技术,实现医疗服务的智能化、高效化和个性化,提升医疗质量,改善患者就医体验。 医学编程与建模作为信息技术在医疗领域的重要应用,对医院人工智能建设起着关键作用。在医疗数据处理方面,医院每天都会产生海量的医疗数据,包括患者的病历、检查检验报告、影像资料等。这些数据蕴含着丰富的信息,但传统的数据处理方式难以对其进行有效分析和利用。医学编程通过开发高效的数据处理算法和软件,可以快速准确地对医疗数据进行清洗、整合和分析,挖掘其中的潜在价值,为医疗决策提供有力支持。例如,利用数据挖掘技术可以从大量的病历数据中发现疾病的发病规律、治疗效果与药物之间的关系等,帮助医生制定更合理的治疗方案。 在疾病诊断与预测领域,医学建模能够建立各种疾病的数学模型,模拟疾病的发生发展过程,辅助医生进行疾病的早期诊断和预测

开源实战——手把手教你搭建AI量化分析平台:从Docker部署到波浪理论实战

开源实战——手把手教你搭建AI量化分析平台:从Docker部署到波浪理论实战

目录 导语 一、 为什么我们需要自己的AI分析工具? 二、 核心部署实战:避坑指南与镜像加速 1.基础环境准备 2.配置 AI 大脑:蓝耘 API 3.进阶技巧:Dockerfile 镜像加速(关键步骤) 4.构建与启动 三、 核心功能深度评测:AI 如何解读波浪理论? 1.AI 股票对话分析:不只是聊天,是逻辑推演 2.模拟交易账户管理:实战演练场 3.历史回测:让数据说话 4.系统设置界面 四、 打造全天候监控体系:通知渠道配置 五、 总结 导语 在量化交易日益普及的今天,散户最缺的往往不是数据,而是对数据的“解读能力”。面对满屏的K线图,