一、为什么需要 Promise?------ 告别回调地狱
在 Promise 出现之前,JavaScript 处理异步操作的主流方式是回调函数。比如我们需要按顺序执行三个异步请求(第一个请求的结果作为第二个的参数,第二个的结果作为第三个的参数),代码会变成这样:
javascript
// 回调地狱示例:按顺序执行三个异步请求
ajax('api/step1', (res1) => {
console.log('第一步完成', res1);
ajax('api/step2?param=' + res1, (res2) => {
console.log('第二步完成', res2);
ajax('api/step3?param=' + res2, (res3) => {
console.log('第三步完成', res3);
}, (err3) => {
console.error('第三步失败', err3);
});
}, (err2) => {
console.error('第二步失败', err2);
});
}, (err1) => {
console.error('第一步失败', err1);
});
这种嵌套层级不断加深的代码,被称为"回调地狱"(Callback Hell),它存在三个致命问题:
-
可读性差:代码横向扩张,逻辑链条被嵌套割裂,后期维护时需要层层拆解才能理清流程;
-
错误处理繁琐:每个异步操作都要单独处理错误,无法统一捕获,容易遗漏异常;
-
可维护性低:修改某个步骤的逻辑时,需要改动多层嵌套的代码,耦合度极高。
而 Promise 的出现,正是为了解决这些问题。它通过"状态管理"和"链式调用",将嵌套的回调改为线性的链式代码,同时提供统一的错误处理机制,让异步逻辑变得清晰可控。
二、Promise 核心概念:什么是"承诺"?
Promise 字面意思是"承诺",在 JS 中,它是一个代表异步操作最终完成或失败的对象。我们可以用一个生动的类比理解:
你去餐厅点餐,服务员给你一个取餐号(Promise 对象)------ 这个取餐号就是餐厅对你的"承诺":你的餐品正在制作(pending 状态),最终要么做好了叫你取餐(fulfilled 状态),要么因为食材不足等原因制作失败(rejected 状态)。在等待期间,你不需要一直盯着厨房,而是可以自由做其他事(非阻塞),直到承诺兑现或失败。
从技术角度,Promise 有三个核心特性:
-
它是一个对象,封装了异步操作的结果;
-
提供统一的 API(then/catch/finally 等),让异步操作的处理方式标准化;
-
状态一旦确定就不可变,确保行为的确定性。
三、Promise 三大状态与状态流转
Promise 的核心是"状态管理",它有且只有三种状态,且状态流转不可逆:
| 状态 | 说明 | 是否可变 |
|---|---|---|
| pending(进行中) | 初始状态,异步操作正在执行,既未成功也未失败 | 是(可转为 fulfilled 或 rejected) |
| fulfilled(已成功) | 异步操作完成,Promise 兑现了承诺 | 否(状态凝固,不可再变) |
| rejected(已失败) | 异步操作失败,Promise 未兑现承诺 | 否(状态凝固,不可再变) |
| 状态流转的唯一路径: |
text
pending(初始)
├─ resolve() → fulfilled(成功)
└─ reject() → rejected(失败)
关键提醒:状态一旦从 pending 转为 fulfilled 或 rejected,就永远无法改变。比如先调用 resolve() 再调用 reject(),reject() 会失效;反之亦然。
示例验证状态不可逆:
javascript
const p = new Promise((resolve, reject) => {
resolve('操作成功'); // 状态转为 fulfilled
reject('操作失败'); // 无效!状态已凝固
});
p.then(res => console.log(res)); // 输出:操作成功
p.catch(err => console.log(err)); // 不会执行
四、Promise 基础用法:从创建到使用
4.1 创建 Promise 实例
通过 new Promise(executor) 创建 Promise 实例,executor 是一个立即执行的函数,接收两个参数:
-
resolve(value):将状态从 pending 转为 fulfilled,并传递成功的结果 value; -
reject(reason):将状态从 pending 转为 rejected,并传递失败的原因 reason(通常是 Error 对象)。
示例:创建一个模拟异步请求的 Promise:
javascript
// 创建 Promise 实例
const fetchData = new Promise((resolve, reject) => {
console.log('Promise 执行器立即执行'); // 同步执行
// 模拟异步操作(比如接口请求)
setTimeout(() => {
const success = Math.random() > 0.5; // 50% 成功概率
if (success) {
resolve({ code: 200, data: '请求成功的数据' }); // 成功:传递结果
} else {
reject(new Error('接口请求超时')); // 失败:传递错误对象
}
}, 1000);
});
注意:Promise 构造函数中的 executor 函数是同步立即执行的,但 resolve/reject 通常在异步操作内部调用,决定最终的状态。
4.2 三种快捷创建方式
除了 new Promise(),JS 还提供了三个静态方法,快速创建指定状态的 Promise:
- Promise.resolve(value):快速创建一个已成功的 Promise(fulfilled 状态)
javascript
// 等价于 new Promise(resolve => resolve(42))
const p1 = Promise.resolve(42);
p1.then(res => console.log(res)); // 输出:42
适用场景:将非 Promise 值包装成 Promise(便于链式调用)、返回已知成功结果的异步操作。
- Promise.reject(reason):快速创建一个已失败的 Promise(rejected 状态)
javascript
// 等价于 new Promise((resolve, reject) => reject(new Error('失败')))
const p2 = Promise.reject(new Error('请求失败'));
p2.catch(err => console.error(err.message)); // 输出:请求失败
- Promise.all(iterable) / Promise.race(iterable):批量处理多个 Promise(后续进阶部分详解)
4.3 消费 Promise:then/catch/finally
创建 Promise 后,需要通过 then、catch、finally 这三个核心方法"消费"它的结果,也就是注册状态改变后的回调函数。
-
then():处理成功状态接收一个 onFulfilled 回调函数,当 Promise 变为 fulfilled 时执行,参数是 resolve 传递的成功结果。同时,then() 会返回一个新的 Promise,支持链式调用。
-
catch():处理失败状态 接收一个 onRejected 回调函数,当 Promise 变为 rejected 时执行,参数是 reject 传递的失败原因。等价于
then(null, onRejected),且能捕获链式调用中任意环节的错误。 -
finally():清理操作接收一个无参数的回调函数,无论 Promise 成功还是失败,都会执行。常用于清理资源(比如关闭加载动画、释放内存)。
示例:完整的 Promise 消费流程:
javascript
fetchData
.then(res => {
console.log('请求成功:', res.data);
return res.data.toUpperCase(); // 返回普通值,传递给下一个 then
})
.then(upperData => {
console.log('处理后的数据:', upperData);
})
.catch(err => {
console.error('请求失败:', err.message); // 捕获任意环节的错误
})
.finally(() => {
console.log('请求结束,关闭加载动画'); // 无论成败都执行
});
五、进阶特性:链式调用与批量处理
5.1 链式调用:线性化异步流程
Promise 最强大的特性之一是"链式调用",它让多个异步操作按顺序执行变得简单。核心原理是:then()/catch()/finally() 都会返回一个新的 Promise,下一个 then 会接收上一个 then 的返回值,并根据返回值类型决定后续行为:
| 上一个 then 的返回值 | 下一个 then 接收的值 |
|---|---|
| 普通值(非 Promise) | 直接接收该普通值 |
| Promise 对象 | 等待该 Promise 状态确定后,接收其 resolve/reject 的值 |
| 抛出错误(throw new Error()) | 错误被后续的 catch 捕获 |
示例:用链式调用重构"回调地狱"的三个异步请求:
javascript
// 链式调用:线性流程,清晰可控
ajax('api/step1')
.then(res1 => {
console.log('第一步完成', res1);
return ajax('api/step2?param=' + res1); // 返回新 Promise,等待其完成
})
.then(res2 => {
console.log('第二步完成', res2);
return ajax('api/step3?param=' + res2);
})
.then(res3 => {
console.log('第三步完成', res3);
})
.catch(err => {
console.error('任意步骤失败:', err); // 统一捕获所有错误
});
对比之前的回调地狱,链式调用的代码线性展开,逻辑清晰,错误处理也只需一次 catch 即可。
5.2 批量处理:Promise.all 与 Promise.race
实际开发中,经常需要处理多个异步操作(比如同时加载多个图片、并行请求多个接口),Promise 提供了两个静态方法实现批量处理:
- Promise.all(iterable):并行执行,全成才成接收一个可迭代对象(如数组),包含多个 Promise;返回一个新 Promise:适用场景:多个独立的异步操作,需要全部完成后再继续(比如表单提交前,验证多个接口数据)。
javascript
// 并行请求两个接口
const fetchUser = fetch('api/user').then(res => res.json());
const fetchGoods = fetch('api/goods').then(res => res.json());
Promise.all([fetchUser, fetchGoods])
.then(([user, goods]) => {
console.log('用户数据:', user);
console.log('商品数据:', goods);
})
.catch(err => {
console.error('任一请求失败:', err);
});
当所有传入的 Promise 都变为 fulfilled 时,新 Promise 才 fulfilled,返回值是所有 Promise resolve 结果的数组(顺序与传入顺序一致);
只要有一个传入的 Promise 变为 rejected,新 Promise 立即 rejected,返回值是第一个 rejected 的原因。
- Promise.race(iterable):竞速执行,先成先得 接收一个可迭代对象,返回一个新 Promise:哪个 Promise 先改变状态(无论成功或失败),新 Promise 就沿用哪个的状态和结果 。
适用场景:处理超时逻辑(比如接口请求超过 5 秒就提示超时)、获取最快响应的资源。
javascript
// 模拟接口请求超时逻辑(5秒超时)
const request = fetch('api/data').then(res => res.json());
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('请求超时')), 5000);
});
Promise.race([request, timeout])
.then(data => console.log('请求成功:', data))
.catch(err => console.error('错误:', err)); // 先触发的状态决定结果
六、实战场景与最佳实践
6.1 常见实战场景
- 接口请求处理:结合 fetch 或 axios(本质是 Promise 封装)处理 API 请求,统一管理加载状态和错误提示。
javascript
async function getUserData() {
try {
const res = await fetch('api/user'); // await 是 Promise 的语法糖
if (!res.ok) throw new Error('接口返回错误');
const data = await res.json();
return data;
} catch (err) {
console.error('获取用户数据失败:', err);
throw err; // 向上抛出,让调用方处理
}
}
- 批量资源加载:用 Promise.all 并行加载多个图片、脚本,提升页面加载效率。
javascript
// 并行加载多张图片
function loadImages(urls) {
const promises = urls.map(url => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`加载图片失败:${url}`));
img.src = url;
});
});
return Promise.all(promises);
}
// 使用
loadImages(['img1.jpg', 'img2.jpg'])
.then(images => console.log('所有图片加载完成', images))
.catch(err => console.error(err));
- 异步任务队列:用链式调用实现按顺序执行的异步任务队列(比如依次处理多个文件上传)。
6.2 总结
-
始终处理错误:每个 Promise 链式调用都要加 catch(),或在 async/await 中用 try/catch,避免"静默错误"导致程序异常。
-
避免嵌套 Promise:即使在 then 中需要新的异步操作,也应返回 Promise 继续链式调用,而非嵌套 new Promise。
-
合理使用 Promise.all:多个独立异步操作优先用 Promise.all 并行执行,提升效率;但如果操作有依赖,需用链式调用串行执行。
-
不滥用 Promise:同步操作无需包装成 Promise;简单的异步操作(如单个定时器),若逻辑简单,可根据情况选择回调,但复杂场景优先用 Promise。
-
用 async/await 简化代码:async/await 是 Promise 的语法糖,能让链式调用的代码更像同步代码,可读性更高(本质还是基于 Promise 实现)。