
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):正在考试,结果未知
代码里就是这样:
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('失败了');
}, 1000);
});
promise.then(console.log).catch(console.error);
为啥说比你对象心情还难猜?因为对象心情还能哄好,Promise 状态变了就是变了,天王老子来了也改不了。所以写代码的时候千万注意,别在 resolve后面又写个 reject,看着挺严谨,实际全是废话。
还有个坑:如果你创建 Promise 的时候忘了调用 resolve或 reject,那它就会一直 pending。就像你给女神发消息,她已读不回,你永远不知道她在想啥。这种"内存泄漏"式 bug 最难查,因为控制台也不报错,就是没反应。
const badPromise = new Promise((resolve, reject) => {
console.log('我开始执行了');
});
badPromise.then(() => {
console.log('这行永远不会执行');
});
怎么发现这种问题?浏览器开发者工具的 Memory 面板可以拍快照,看看有没有一直 pending 的 Promise 对象。或者简单点,给 Promise 加个超时逻辑:
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);
}, 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);
});
看到没?错误处理统一了,缩进也正常了。而且 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);
});
看起来挺美,但实际项目里这么用,分分钟翻车。比如用户订单接口挂了,但用户信息和消息是好的,这时候你直接显示"加载失败",用户体验极差。更惨的是,如果三个接口里有一个特别慢,用户得等最慢的那个完事才能看到页面。
所以 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[]. : [];
messages = results[]. === ? results[]. : [];
({ user, orders, messages });
});
看到没?即使中间那个接口挂了,其他的数据照样能拿到。返回的数组里,每个元素是个对象,有 status字段标记状态,value存成功结果,reason存失败原因。
实际项目里,我基本都是用 allSettled,除非业务上确实要求"一个失败全失败"。比如支付流程,扣款和生成订单必须都成功,这时候才用 all。
Promise.race 玩的就是心跳,谁快听谁的
字面意思:赛跑,谁第一个 settled(不管是成功还是失败),就听谁的。
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));
const loadScript = src => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script. = src;
script. = resolve;
script. = reject;
..(script);
});
};
cdn1 = ;
cdn2 = ;
cdn3 = ;
.([(cdn1), (cdn2), (cdn3)])
.( .())
.( .());
注意: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.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() {
const criticalData = await Promise.all([
fetchUserInfo(),
fetchPermissions()
]).catch(err => {
showErrorPage('系统初始化失败,请刷新重试');
throw err;
});
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 : [];
Promise.race([
loadStatsScriptFromCDN1(),
loadStatsScriptFromCDN2()
]).catch(() => {
console.();
});
(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、重置状态');
});
};
doSomething().then(console.log).catch(console.error);
实际项目中最常见的用法就是关闭 loading:
class ApiService {
async request(url, options = {}) {
this.showLoading();
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();
}
}
showLoading() {
document.getElementById('loading').style.display = 'block';
}
hideLoading() {
document.getElementById('loading').style.display = 'none';
}
handleError(error) {
console.error('API 错误:', error);
}
}
为啥 finally 里不能改数据,踩过的坑说出来都是泪
finally里可以写代码,但它的返回值不会影响整个 Promise 的结果。也就是说,你在 finally里 return 啥都没用,该返回啥还是返回啥。
Promise.resolve('原始数据')
.then(data => {
console.log('then:', data);
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 => {
return { ...processedData, flag: true };
});
fetchData()
.then(data => {
const processed = process(data);
return { ...processed, flag: true };
})
.finally(() => {
cleanup();
});
关闭 loading、清理定时器这些活儿交给它最合适
除了 loading,这些场景也适合放 finally:
function fetchWithTimeout(url, ms) {
let timer;
return Promise.race([
fetch(url),
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('超时')), ms);
})
]).finally(() => {
clearTimeout(timer);
});
}
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.);
error;
} {
. = ;
}
}
}
() {
btn = .();
btn. = ;
btn. = ;
{
();
} {
btn. = ;
btn. = ;
}
}
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);
});
看到没?本来 resolved 的 Promise,因为 finally里抛了错,变成了 rejected。这点要特别注意,finally里尽量别抛错,除非你真的想中断流程。
还有更隐蔽的坑:finally里如果返回一个 rejected 的 Promise,效果等同于抛错:
Promise.resolve('正常')
.finally(() => {
return Promise.reject('finally 返回的 reject');
})
.catch(err => console.error(err));
所以 finally的最佳实践是:只做同步的清理工作,别搞异步操作,更别抛错。
链式调用写爽了是真的爽
Promise 的精髓就是链式调用,.then().then().then()看着就舒服。但写爽了容易飘,一飘就翻车。
then 接 then 像串糖葫芦,数据一层层往下传
每个 then都可以接收上一个 then的返回值:
getUserId()
.then(userId => {
console.log('拿到 userId:', userId);
return getUserInfo(userId);
})
.then(userInfo => {
console.log('拿到 userInfo:', userInfo);
return getOrders(userInfo.id);
})
.then(orders => {
console.log('拿到 orders:', orders);
return orders.length;
})
.then(count => {
console.log('订单数量:', count);
});
关键点:一定要 return! 如果不 return,下一个 then拿到的是 undefined:
getUserId()
.then(userId => {
getUserInfo(userId);
})
.then(userInfo => {
console.log(userInfo);
});
这种 bug 最难查,因为控制台不报错,只是数据不对。建议写 then的时候先写 return,再填内容。
catch 放哪有讲究,放错了 bug 找到你头秃
catch能捕获它前面所有 then的错误,所以位置很重要:
fetchUser()
.then(user => fetchOrders(user.id))
.then(orders => fetchDetails(orders[0].id))
.then(details => render(details))
.catch(err => {
console.error(' somewhere 错了:', err);
});
fetchUser()
.then(user => fetchOrders(user.id))
.catch(err => {
console.error('获取用户或订单失败:', err);
return [];
})
.then(orders => {
return fetchDetails(orders[0]?.id);
})
.then(details => render(details))
.catch( => {
.(, err);
});
实际项目中,我经常用这种"中间 catch"做容错:
Promise.all([
fetchCriticalData().catch(err => {
throw err;
}),
fetchModuleA().catch(err => {
console.warn('模块 A 加载失败:', err);
return 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 function loadAndRender() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
const details = await Promise.all(orders.map(o => fetchDetail(o.id)));
activeItems = details.( d. === );
(activeItems);
} (err) {
.(err);
}
}
但注意,async/await 不是万能的。比如你想并行执行请求,还是得用 Promise.all:
const user = await fetchUser();
const orders = await fetchOrders(user.id);
const messages = await fetchMessages(user.id);
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));
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; }
});
});
});
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。渐进式改造:
function promisifyAjax(url, options) {
return new Promise((resolve, reject) => {
$.ajax({
url,
...options,
success: resolve,
error: (xhr, status, err) => reject(err)
});
});
}
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 };
({ user, orders, stats });
} {
(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));
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) {
( {
xhr = ();
xhr..(, {
(e.) {
percent = (e. / e.) * ;
.(file., percent);
}
});
xhr.(, {
(xhr. === ) {
.++;
.();
({ : file., : xhr. });
} {
( ());
}
});
xhr.(, {
.++;
.();
( ());
});
xhr.(, );
xhr.(file);
});
}
() {
.();
}
() {
totalPercent = ((. + .) / .) * ;
.();
}
}
files = .().;
uploader = (.(files));
uploader.().( {
.(, 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 {
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.);
(attempt < maxRetries) {
( (resolve, retryDelay));
}
}
}
();
}
(, { : , : , : })
.( .(, data))
.( .(, 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 => {
localStorage.setItem('token', data.token);
return data.user;
})
.then(user => {
return this.fetchPermissions(user.id).then(permissions => ({ ...user, permissions }));
})
.then(userWithPerms => {
return this.fetchUnreadCount().( ({ ...userWithPerms, : count }));
})
.( {
.(finalUser);
finalUser;
})
.( {
.();
err;
});
}
() {
res = ();
res.();
}
() {
res = ();
data = res.();
data.;
}
() {
.?.(, user);
}
}
auth = ();
auth.({ : , : })
.( {
.(, user);
router.();
})
.( {
(err.);
});
出问题了咋排查
Promise pending 了不往下走,大概率是忘了 resolve
症状:代码执行了,但 then里的逻辑没触发,也不报错。
检查点:
- 是否忘了调用
resolve/reject
- 是否把
resolve写成了 reslove(拼写错误)
- 是否在
new Promise里抛了同步错误(这种会直接 reject,但不会走 then)
new Promise((resolve) => {
setTimeout(() => {
console.log('时间到了');
}, 1000);
}).then(() => console.log('这行不会执行'));
new Promise((resolve) => {
reslove('数据');
}).then(console.log);
new Promise(() => {
throw new Error('同步错误');
}).catch(err => console.log('会进这里:', err));
错误吞掉了没报错,检查 catch 有没有漏
Promise 的错误如果没有被 catch,会报"Uncaught (in promise)",但有时候你明明写了 catch,错误还是没了,检查:
catch里是不是忘了继续抛出错误?
then里是不是返回了 undefined,导致后续逻辑没数据?
fetchData()
.then(data => process(data))
.catch(err => {
console.error(err);
})
.then(result => {
saveToDatabase(result);
});
fetchData()
.then(data => process(data))
.catch(err => {
console.error(err);
throw err;
})
.then(result => {
saveToDatabase(result);
});
多个请求顺序乱了,看看是不是该用 all 还是 race
如果请求 A 依赖请求 B 的数据,千万别用 Promise.all并行执行,要串行:
Promise.all([
fetchUser(),
fetchOrders(userId)
]);
fetchUser().then(user => fetchOrders(user.id));
const user = await fetchUser();
const orders = await fetchOrders(user.id);
浏览器控制台怎么调试 Promise,几个小技巧
- 在 then 里打断点:直接在代码里写
debugger,或者 Chrome 开发者工具里点行号
- 查看 Promise 状态:在 Console 面板输入 Promise 实例,可以看到
[[PromiseState]]和 [[PromiseResult]]
- 用 console.log 追踪:在每个
then开头打印数据,看流向
- Network 面板:确认请求是否真的发出去了,状态码对不对
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
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);
.();
请求队列控制并发数,别把服务器打崩了
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();
});
}
}
queue = ();
files = .({ : }, );
uploadPromises = files.( queue.( (file)));
.(uploadPromises);
.();
自己手写一个简易 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());
}
};
{
(resolve, reject);
} (err) {
(err);
}
}
() {
( {
(. === ) {
( {
{
x = (.);
(x);
} (err) {
(err);
}
});
} (. === ) {
( {
{
x = (.);
(x);
} (err) {
(err);
}
});
} {
..( {
( {
{
x = (.);
(x);
} (err) {
(err);
}
});
});
..( {
( {
{
x = (.);
(x);
} (err) {
(err);
}
});
});
}
});
}
}
( {
( (), );
})
.( {
.(, value);
;
})
.( {
.(value);
});
把回调风格的老代码改成 Promise,渐进式改造
const fs = require('fs');
function readFileCallback(path, callback) {
fs.readFile(path, 'utf8', (err, data) => {
if (err) {
callback(err);
return;
}
callback(null, data);
});
}
function readFilePromise(path) {
return new Promise((resolve, reject) => {
readFileCallback(path, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
}
readFile = (fs.);
content = (, );
最后唠两句
写这篇的时候,我又想起那个凌晨三点的厕所隔间。那时候我觉得 Promise 是世界上最难的东西,现在回头看,其实就那么几个概念:状态、then、catch、finally,再加上 all/race 这些工具方法。
但编程就是这样,难的不是概念,是细节。哪个 then 忘了 return、catch 放错位置、finally 里想改数据…这些坑不踩一遍,看十遍文档也记不住。所以我把这些血泪史都写出来了,能帮你少熬几个夜,这篇就没白写。
代码我都跑过,但环境不同可能会有差异。如果你复制粘贴发现有问题,先检查浏览器版本(特别是用 any/allSettled 这些新 API 的时候),再看看网络请求是不是真的通了。