欢迎光临
我们一直在努力

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

在这里插入图片描述

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

  • 前端必会:Promise多请求+finally+链式调用避坑指南
    • 先说点掏心窝子的
      • 当年我被Promise坑到怀疑人生的那些夜晚
      • 为啥现在还在讲Promise?这玩意儿过时了吗
      • 看完这篇你能少加多少班,心里有点数
    • Promise这货到底是个啥
      • 别整那些虚的,用大白话讲清楚Promise是干啥的
      • 三种状态来回切换,比你对象心情还难猜
      • 从回调地狱到链式调用,前端人的血泪进化史
    • 多个请求一起搞,姿势要对
      • Promise.all一把梭,全成功才返回,失败直接凉凉
      • Promise.allSettled才是亲儿子,不管成败都能拿到结果
      • Promise.race玩的就是心跳,谁快听谁的
      • Promise.any新出的狠角色,只要有一个成功就行
      • 实际项目中咋选,看场景别瞎用
    • finally这块儿真容易翻车
      • finally不管成功失败都会执行,这点得刻在脑子里
      • 为啥finally里不能改数据,踩过的坑说出来都是泪
      • 关闭loading、清理定时器这些活儿交给它最合适
      • finally返回的值会不会影响链式,实测给你看
    • 链式调用写爽了是真的爽
      • then接then像串糖葫芦,数据一层层往下传
      • catch放哪有讲究,放错了bug找到你头秃
      • 链子太长怎么办,async/await来救场
      • 别再then里面嵌套then了,求你了
    • 这玩意儿好在哪,坑又在哪
      • 代码看着清爽多了,回调地狱终于能告别
      • 错误处理统一了,不用到处try-catch
      • 学习曲线有点陡,刚开始真的容易懵
      • 老项目改造成本高,别指望一天搞定
    • 实际干活时咋用
      • 接口批量请求,用户信息+订单数据一起拉
      • 上传多个文件,进度条怎么搞
      • 请求超时+重试机制,finally在这里派上用场
      • 登录鉴权流程,链式调用把逻辑串起来
    • 出问题了咋排查
      • Promisepending了不往下走,大概率是忘了resolve
      • 错误吞掉了没报错,检查catch有没有漏
      • 多个请求顺序乱了,看看是不是该用all还是race
      • 浏览器控制台怎么调试Promise,几个小技巧
      • async/await混用时报错信息看不懂,教你怎么定位
    • 老手都不一定知道的骚操作
      • 用Promise封装定时器,setInterval也能链式
      • 请求队列控制并发数,别把服务器打崩了
      • 自己手写一个简易Promise,理解更透彻
      • 把回调风格的老代码改成Promise,渐进式改造
    • 最后唠两句

前端必会: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分就不看电视",这个保证有三种结局:

  • 兑现了(fulfilled):真考了90分,奖励你吃顿好的
  • 没兑现(rejected):考了59分,混合双打伺候
  • 还没考呢(pending):正在考试,结果未知
  • 代码里就是这样:

    // 创建一个Promise,相当于写下保证书
    const myPromise = new Promise((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); // 考砸了走这里
    });

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

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

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

    • Pending(等待中):刚创建的时候,就像你刚发消息给女神,她还没回
    • Fulfilled(已成功):女神回"好的",你心花怒放
    • Rejected(已失败):女神回"滚",你万念俱灰

    const promise = new Promise((resolve, reject) => {
    console.log(promise); // 这里打印不出来,因为创建时还没赋值

    setTimeout(() => {
    resolve('成功了');
    // 注意:这里再reject已经没用了,状态只会变一次
    reject('失败了'); // 这行不会执行,或者执行了也无效
    }, 1000);
    });

    // 状态一旦确定,再操作也没用
    promise.then(console.log).catch(console.error);

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

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

    // 错误示范:永远pending的Promise
    const badPromise = new Promise((resolve, reject) => {
    console.log('我开始执行了');
    // 完了,忘了resolve了!
    });

    badPromise.then(() => {
    console.log('这行永远不会执行');
    });

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

    // 给Promise加超时,防止一直pending
    function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('超时了兄弟!')), 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 = new Promise(resolve => setTimeout(() => resolve('慢'), 1000));
    const fast = new Promise(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:请求超时控制
    const fetchWithTimeout = (url, ms = 5000) => {
    const fetchPromise = fetch(url);
    const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('请求超时')), 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地址,谁快用谁
    const loadScript = (src) => {
    return new Promise((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 第一个成功 全部失败 多源备份、容错加载

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

    // 首页初始化,分三类请求
    async function initHomePage() {
    // 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不管成功失败都会执行,这点得刻在脑子里

    const doSomething = () => {
    return fetch('/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:

    class ApiService {
    async request(url, options = {}) {
    this.showLoading(); // 显示loading

    try {
    const response = await fetch(url, options);
    if (!response.ok) throw new Error('HTTP ' + response.status);
    return await 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 => {
    // 处理数据
    return process(data);
    })
    .finally(processedData => { // 注意:finally回调没有参数!
    // 想在这里再加个标记,结果processedData是undefined
    return { processedData, flag: true }; // 完全没用
    });

    // 正确做法
    fetchData()
    .then(data => {
    const processed = process(data);
    return { processed, flag: true }; // 在then里处理好
    })
    .finally(() => {
    // 这里只干清理工作
    cleanup();
    });

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

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

    // 场景1:清理定时器
    function fetchWithTimeout(url, ms) {
    let timer;

    return Promise.race([
    fetch(url),
    new Promise((_, reject) => {
    timer = setTimeout(() => reject(new Error('超时')), ms);
    })
    ]).finally(() => {
    clearTimeout(timer); // 不管成功失败,定时器都得清,不然内存泄漏
    });
    }

    // 场景2:释放锁(比如防止重复提交)
    class SubmitController {
    constructor() {
    this.isSubmitting = false;
    }

    async submit(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:恢复按钮状态
    async function handleClick() {
    const btn = document.getElementById('submitBtn');
    btn.disabled = true;
    btn.textContent = '提交中…';

    try {
    await submitForm();
    } finally {
    // 不管成功失败,按钮都得恢复
    btn.disabled = false;
    btn.textContent = '提交';
    }
    }

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

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

    Promise.resolve('一切正常')
    .finally(() => {
    console.log('finally执行');
    throw new Error('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); // 123
    return getUserInfo(userId); // 返回新的Promise
    })
    .then(userInfo => {
    console.log('拿到userInfo:', userInfo); // {name: '张三', id: 123}
    return getOrders(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是[]
    return fetchDetails(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);
    return null; // 返回null,页面继续渲染
    }),

    fetchModuleB().catch(err => {
    console.warn('模块B加载失败:', err);
    return null;
    })
    ]).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写法,更像同步代码
    async function loadAndRender() {
    try {
    const user = await fetchUser();
    const orders = await fetchOrders(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 = await fetchUser();
    const orders = await fetchOrders(user.id); // 等user回来才发请求
    const messages = await fetchMessages(user.id); // 等orders回来才发请求

    // 正确:并行执行orders和messages
    const user = await fetchUser();
    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/await
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);
    const details = await fetchDetails(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。

    建议学习路径:

  • 先理解三种状态和基本用法
  • 练习链式调用,重点练"return"
  • 学多请求处理(all/race等)
  • 最后学async/await,对比着学
  • 老项目改造成本高,别指望一天搞定

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

    // 第一步:封装现有回调为Promise
    function promisifyAjax(url, options) {
    return new Promise((resolve, reject) => {
    $.ajax({
    url,
    options,
    success: resolve,
    error: (xhr, status, err) => reject(err)
    });
    });
    }

    // 第二步:新代码用Promise写法
    promisifyAjax('/api/data')
    .then(data => console.log(data));

    // 第三步:旧代码逐步替换,别一次性全改,容易出大事


    实际干活时咋用

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

    // 页面初始化,拉取核心数据
    async function initDashboard() {
    const loading = showLoading();

    try {
    // 并行请求,但做容错处理
    const [userResult, ordersResult, statsResult] = await Promise.allSettled([
    fetchUser(),
    fetchOrders(),
    fetchStats()
    ]);

    // 处理用户数据(关键,失败则整页报错)
    if (userResult.status === 'rejected') {
    throw new Error('获取用户信息失败:' + 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);
    }
    }

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

    class FileUploader {
    constructor(files) {
    this.files = files;
    this.total = files.length;
    this.completed = 0;
    this.failed = 0;
    }

    async uploadAll() {
    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) {
    return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    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(new Error(`${file.name} 上传失败: ${xhr.status}`));
    }
    });

    xhr.addEventListener('error', () => {
    this.failed++;
    this.updateTotalProgress();
    reject(new Error(`${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 = new FileUploader(Array.from(files));
    uploader.uploadAll().then(result => {
    console.log('上传完成:', result);
    });

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

    async function fetchWithRetry(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 = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
    const response = await fetch(url, {
    options,
    signal: controller.signal
    });

    if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
    } finally {
    // 无论如何都要清定时器,避免内存泄漏
    clearTimeout(timeoutId);
    }

    } catch (error) {
    lastError = error;
    console.warn(`${attempt}次失败:`, error.message);

    if (attempt < maxRetries) {
    // 等待一段时间再重试
    await new Promise(resolve => setTimeout(resolve, retryDelay));
    }
    }
    }

    throw new Error(`重试${maxRetries}次后仍然失败: ${lastError.message}`);
    }

    // 使用
    fetchWithRetry('/api/unstable-endpoint', {
    maxRetries: 3,
    timeout: 3000,
    retryDelay: 500
    })
    .then(data => console.log('成功:', data))
    .catch(err => console.error('彻底失败:', err));

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

    class AuthService {
    async login(credentials) {
    return fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify(credentials)
    })
    .then(res => {
    if (!res.ok) throw new Error('登录失败');
    return res.json();
    })
    .then(data => {
    // 保存token
    localStorage.setItem('token', data.token);
    return data.user;
    })
    .then(user => {
    // 获取用户权限
    return this.fetchPermissions(user.id)
    .then(permissions => ({ user, permissions }));
    })
    .then(userWithPerms => {
    // 获取未读消息
    return this.fetchUnreadCount()
    .then(count => ({ userWithPerms, unreadCount: count }));
    })
    .then(finalUser => {
    // 更新全局状态
    this.updateGlobalState(finalUser);
    return finalUser;
    })
    .catch(err => {
    // 清理可能残留的token
    localStorage.removeItem('token');
    throw err;
    });
    }

    async fetchPermissions(userId) {
    const res = await fetch(`/api/users/${userId}/permissions`);
    return res.json();
    }

    async fetchUnreadCount() {
    const res = await fetch('/api/messages/unread');
    const data = await res.json();
    return data.count;
    }

    updateGlobalState(user) {
    window.$store?.commit('setUser', user);
    }
    }

    // 使用
    const auth = new AuthService();
    auth.login({ username: 'admin', password: '123456' })
    .then(user => {
    console.log('登录成功:', user);
    router.push('/dashboard');
    })
    .catch(err => {
    alert(err.message);
    });


    出问题了咋排查

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

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

    检查点:

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

    // 错误:拼写错误
    new Promise((resolve) => {
    reslove('数据'); // 拼写错误,resolve没调用
    }).then(console.log);

    // 错误:同步抛错
    new Promise(() => {
    throw new Error('同步错误');
    }).catch(err => console.log('会进这里:', err));

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

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

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

    // 正确:需要继续抛的错误要throw
    fetchData()
    .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/await
    const user = await fetchUser();
    const orders = await fetchOrders(user.id);

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

  • 在then里打断点:直接在代码里写debugger,或者Chrome开发者工具里点行号
  • 查看Promise状态:在Console面板输入Promise实例,可以看到[[PromiseState]]和[[PromiseResult]]
  • 用console.log追踪:在每个then开头打印数据,看流向
  • Network面板:确认请求是否真的发出去了,状态码对不对
  • // 调试技巧:给Promise加日志
    const debugPromise = (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,错误栈可能很长。看错误信息时:

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


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

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

    // 延迟执行
    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    // 使用
    async function demo() {
    console.log('开始');
    await delay(1000);
    console.log('1秒后');
    await delay(1000);
    console.log('又1秒后');
    }

    // 轮询直到条件满足
    async function pollUntil(conditionFn, interval = 1000, maxAttempts = 10) {
    for (let i = 0; i < maxAttempts; i++) {
    if (conditionFn()) {
    return true;
    }
    await delay(interval);
    }
    throw new Error('轮询超时');
    }

    // 使用:等待某个元素出现
    await pollUntil(() => document.getElementById('app') !== null);
    console.log('app元素出现了');

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

    class PromiseQueue {
    constructor(concurrency = 3) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
    }

    add(promiseFactory) {
    return new Promise((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 = new PromiseQueue(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,理解更透彻

    class MyPromise {
    constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
    if (this.state === 'pending') {
    this.state = 'fulfilled';
    this.value = value;
    this.onFulfilledCallbacks.forEach(fn => fn());
    }
    };

    const reject = (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) {
    return new MyPromise((resolve, reject) => {
    if (this.state === 'fulfilled') {
    setTimeout(() => {
    try {
    const x = onFulfilled(this.value);
    resolve(x);
    } catch (err) {
    reject(err);
    }
    });
    } else if (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) {
    return this.then(null, onRejected);
    }
    }

    // 测试
    new MyPromise((resolve) => {
    setTimeout(() => resolve('成功'), 100);
    }).then(value => {
    console.log('MyPromise:', value);
    return '链式调用';
    }).then(value => {
    console.log(value);
    });

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

    // 老代码:Node.js风格的回调
    const fs = require('fs');

    function readFileCallback(path, callback) {
    fs.readFile(path, 'utf8', (err, data) => {
    if (err) {
    callback(err);
    return;
    }
    callback(null, data);
    });
    }

    // 改造:包装为Promise
    function readFilePromise(path) {
    return new Promise((resolve, reject) => {
    readFileCallback(path, (err, data) => {
    if (err) reject(err);
    else resolve(data);
    });
    });
    }

    // 更通用的promisify工具
    function promisify(fn) {
    return function(args) {
    return new Promise((resolve, reject) => {
    fn(args, (err, result) => {
    if (err) reject(err);
    else resolve(result);
    });
    });
    };
    }

    // 使用
    const readFile = promisify(fs.readFile);
    const content = await readFile('./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等工具

    吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤 非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 前端必会:Promise多请求+finally+链式调用避坑指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址