Promise 多请求 finally 链式调用避坑指南

背景介绍
常见误区与踩坑经历
在异步编程中,Promise 是处理异步操作的核心方案。早期开发者常因嵌套回调导致代码难以维护(Callback Hell)。例如,多个接口依赖调用时,若未正确处理状态流转,极易出现数据获取失败或流程中断的问题。
Promise 在现代开发中的地位
尽管 async/await 语法糖流行,但 Promise 仍是底层基础。主流库如 axios、fetch 及框架响应式系统均基于 Promise 实现。理解其原理有助于排查复杂异步错误。
学习收益
掌握 Promise 可解决以下场景:
- 页面初始化多接口并行加载
- 文件上传进度控制
- 接口超时自动重试
- 统一清理 loading 状态
Promise 基础概念
Promise 定义与原理
Promise 对象代表一个异步操作的最终完成或失败。它包含三种状态:Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)。状态一旦改变便不可逆。
const myPromise = new Promise((resolve, reject) => {
const score = Math.random() > 0.5 ? 95 : 58;
if (score >= 90) {
resolve('考试通过');
} else {
reject('考试失败');
}
});
myPromise
.then(result => console.log(result))
.catch(error => console.error(error));
状态流转机制
创建时默认为 Pending。调用 resolve 转为 Fulfilled,调用 reject 转为 Rejected。若未调用任一方法,Promise 将永久 Pending。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
}, 1000);
});
异步编程演进
无 Promise 时,多层回调嵌套导致缩进过深且错误处理分散。Promise 通过链式调用 .then() 和统一的 .catch() 简化了逻辑结构。
并发请求处理策略
Promise.all 用法
接收 Promise 数组,全部成功则返回结果数组,任意失败则进入 catch。
Promise.all([p1, p2, p3])
.then(([res1, res2, res3]) => {
})
.catch(err => {
});
Promise.allSettled 用法
ES2020 新增,等待所有 Promise 结束,无论成败。返回包含 status 和 value/reason 的数组。
Promise.allSettled(promises).then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`第${index}个成功`);
} else {
console.error(`第${index}个失败`);
}
});
});
Promise.race 用法
返回第一个 settled(成功或失败)的 Promise 结果。常用于超时控制。
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('超时')), ms);
});
return Promise.race([promise, timeout]);
}
Promise.any 用法
只要有一个 Promise 成功即返回结果,全部失败才报错。适用于多源容错场景。
场景选择建议
| 方法 | 成功条件 | 适用场景 |
|---|
| Promise.all | 全部成功 | 强依赖模块 |
| Promise.allSettled | 全部完成 | 弱依赖模块 |
| Promise.race | 首个完成 | 超时控制 |
| Promise.any | 首个成功 | 多源备份 |
finally 块注意事项
finally 执行时机
finally 块中的代码无论 Promise 状态如何都会执行。常用于资源清理。
request()
.then(res => process(res))
.catch(err => handleError(err))
.finally(() => {
hideLoading();
});
finally 返回值限制
finally 的返回值不会影响 Promise 的最终状态。若需修改数据,应在 then 中处理。
Promise.resolve('data')
.then(data => data + '-processed')
.finally(() => 'ignored')
.then(result => console.log(result));
典型应用场景
- 关闭 Loading 动画
- 清除定时器
- 释放锁状态
finally 对链式的影响
在 finally 中抛出错误会改变 Promise 状态为 rejected。
Promise.resolve('ok').finally(() => {
throw new Error('error in finally');
}).catch(err => console.error(err.message));
链式调用规范
链式数据传递
每个 then 应 return 新的 Promise 或值,以便下一环接收。
getUser()
.then(user => getUserInfo(user.id))
.then(info => getOrders(info.id))
.then(orders => render(orders));
错误捕获位置
catch 可放在链尾捕获所有错误,也可在中间拦截特定错误并继续执行。
async/await 替代方案
对于深层嵌套,async/await 提供更线性的代码结构。
async function loadData() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
render(orders);
} catch (err) {
console.error(err);
}
}
避免嵌套 then
严禁在 then 内部嵌套新的 then,应改为链式或 async/await。
优缺点分析
可读性提升
消除了回调地狱,代码纵向排列更清晰。
统一错误处理
集中管理异常,减少重复代码。
学习成本
需理解状态机模型及返回值规则。
渐进式改造
老项目可通过封装回调函数逐步迁移至 Promise。
实战场景
接口批量请求
使用 allSettled 处理关键与非关键数据混合加载。
async function initDashboard() {
const [userRes, ordersRes] = await Promise.allSettled([
fetchUser(),
fetchOrders()
]);
}
文件上传进度
结合 XMLHttpRequest 事件监听与 Promise 封装进度条。
超时与重试
利用 race 实现超时,循环配合 delay 实现重试。
登录鉴权流程
串联登录、权限获取、消息通知等步骤。
问题排查
Pending 状态排查
检查是否遗漏 resolve/reject 调用,或存在拼写错误。
错误捕获遗漏
确保 catch 存在于链中,或手动 throw 错误以中断流程。
请求顺序控制
依赖关系强的请求必须串行执行,使用 await 或 then 链。
调试技巧
使用 debugger 断点,或在 then 中添加 console.log 追踪数据流。
进阶技巧
定时器封装
将 setTimeout/setInterval 包装为 Promise 支持链式调用。
并发队列控制
自定义类限制同时运行的 Promise 数量,防止服务器过载。
手写简易 Promise
通过实现 then 方法和状态管理深入理解原理。
回调转 Promise
使用 promisify 工具函数将 Node.js 风格回调转换为 Promise。
总结
Promise 是现代 JavaScript 异步编程的基石。正确理解其状态流转、组合方法及 finally 特性,能有效避免常见陷阱。在实际开发中,应根据业务场景选择合适的并发处理方式,并结合 async/await 提升代码可读性。