Promise 就是解决回调地狱的对象,把异步操作包成"承诺",成功走 then 失败走 catch。
核心三个状态:
1. pending——进行中
2. fulfilled——成功,走 then
3. rejected——失败,走 catch
坑:Promise 一旦状态变了就不变、then 返回新 Promise 可链式、catch 后面还能 then。
为什么需要它?因为原生 AJAX 靠回调函数处理结果,一旦有好几个接口要"排队一个接一个调",代码就会一层套一层往右缩进,变成传说中的"回调地狱"。Promise 的目标就是把这堆嵌套拍平,让代码从上到下一行一行读,不再嵌套。
假设你要做一个操作:先登录拿到用户 ID → 用用户 ID 查订单 → 用订单 ID 查物流。三个接口有依赖关系,第二个要第一个的结果,第三个要第二个的结果。用原生 AJAX 写,就得一层套一层:
// 第一层:先请求用户信息
// 叫一个小哥去拿用户信息
let xhr1 = new XMLHttpRequest();
// 告诉他去 /user 地址用 GET 拿
xhr1.open("get", "/user", true);
// 货到了打电话给你
xhr1.onreadystatechange = function () {
// 4=货拿完了 且 200=服务器说没问题
if (xhr1.readyState === 4 && xhr1.status === 200) {
// 翻译回信,拿到用户 id——.id 是属性不加括号
let userId = JSON.parse(xhr1.responseText).id;
// 第二层:用 userId 查订单(嵌套在第一层里面)
// 再叫一个小哥,去拿订单
let xhr2 = new XMLHttpRequest();
// 把刚拿到的 userId 拼进地址
xhr2.open("get", "/order?uid=" + userId, true);
// 又一层回调
xhr2.onreadystatechange = function () {
// 又判断一次状态码
if (xhr2.readyState === 4 && xhr2.status === 200) {
// 拿到订单 id——.orderId 是属性不加括号
let orderId = JSON.parse(xhr2.responseText).orderId;
// 第三层:用 orderId 查物流(嵌套在第二层里面)
// 第三个小哥,去查物流
let xhr3 = new XMLHttpRequest();
// 把订单 id 拼进地址
xhr3.open("get", "/logistic?oid=" + orderId, true);
// 又又一层回调
xhr3.onreadystatechange = function () {
// 第三层也判断状态码
if (xhr3.readyState === 4 && xhr3.status === 200) {
// 拿到物流数据
console.log("物流信息拿到了");
}
};
// 第三个小哥出发
xhr3.send();
}
};
// 第二个小哥出发
xhr2.send();
}
};
// 第一个小哥出发——代码写了半天才到这里
xhr1.send();
这就是回调地狱——代码像洋葱一样一层套一层。四大弊端:1. 读着累:从左到右好长一串眼睛疼;2. 改着烦:中间改一个,外面全得跟着动;3. 错误处理乱:每层都要单独判状态码;4. 不能复用:逻辑全缠在一起。Promise 就是来收拾这个烂摊子的。
Promise 是 ES6 新增的异步解决方案,专门用来替代回调嵌套。你可以把它想成"一个承诺包裹":你把异步操作(比如 AJAX 请求)放进去,它会给你一个"承诺"——将来会告诉你成功还是失败。
Promise 有三种状态,像人生一样只能往前走不能回头:
// Promise 三种状态(不可逆!):
// 1. pending 等待态:还在办事中(请求还没回来)
// → 类比:你刚取了号,还在等叫号
// 2. fulfilled 成功态:事办成了,结果没问题
// → 类比:叫到你了,业务办好了
// 3. rejected 失败态:事搞砸了(404、500、断网)
// → 类比:叫到你了,但业务办不了
// 状态只能从 pending 变到 fulfilled 或 rejected
// 一旦变了,就永远定下来了,不能再变回去!
// (不能反悔!不能从成功再变回等待!)
// 创建一个 Promise:
// 取号!创建承诺包裹,传两个函数参数
new Promise(function (resolve, reject) {
// resolve(数据):说"成功了",把数据传出去
// reject(错误信息):说"失败了",把错误传出去
// 内部写异步逻辑(AJAX、定时器等)
// 取完号,等着叫号
});
如果你调了 resolve 又调了 reject——因为状态不可逆,只有第一个生效,第二个直接被忽略。
Promise 本身只是个"承诺包裹",你得用 then 来接成功的结果,用 catch 来接失败的结果。最香的是:then 可以链式调用,每次 return 的值会传给下一个 then,把回调地狱拍平!
// getUser 是一个返回 Promise 的函数
// 第一步:调接口拿用户信息
getUser
// 第一步成功了,拿到用户对象
.then(function (user) {
// 第一步成功:拿到用户
// return 的东西会传给下一个 then——接力棒交出去(
return user.id;
})
// 第二步:拿到上一步传来的 uid
.then(function (uid) {
// 第二步:用 uid 查订单(不再嵌套!链式往下走)
// 拿着 uid 去查订单,结果传给下一个 then
return getOrder(uid);
})
// 第三步:拿到订单
.then(function (order) {
// 第三步:查物流
// 打印最终结果
console.log(order);
})
// 上面任何一步失败,都跑到这里来
.catch(function (err) {
// 上面任何一步失败,都会统一跑到这里来!
// 统一处理错误,不用每层单独判断
console.log("哪里出错了:", err);
// 链尾加 catch 兜底
});
每次 then 里 return 的值,会自动传给下一个 then 的参数——就像接力赛,一个人跑完把接力棒给下一个人。错误不用每层单独判断,全部跑到最后一个 catch 里统一处理。
可能在什么地方用:
1. 多个接口有依赖关系(登录→查订单→查物流),用 Promise 链式拍平嵌套
2. 把原生 AJAX 封装成返回 Promise 的函数
3. 配合定时器、文件读取等异步操作。
常见的问题:
1. 创建了 Promise 但忘了调 resolve/reject,then 永远不执行
2. 在 then 里忘了 return,下一个 then 拿到 undefined
3. 没写 catch,失败时静默崩溃,页面没反应
4. 以为 Promise 状态能反悔(从成功变回等待)——其实不行,不可逆。
解决思路:
1. 创建 new Promise 后,确保成功时一定调 resolve、失败时一定调 reject
2. 链式调用时记住在 then 里 return
3. 永远在链尾加 .catch 兜底
4. 调试时在每个 then 里 console.log 一下,看哪一步没走到。
练习一:体验并改造回调地狱
1. 用原生 AJAX 三层嵌套实现"登录→会员列表→会员详情"串行请求,观察代码缩进
2. 用 Promise 链式调用改造
3. 用 async/await 再改造一版。
练习二:Promise 基础创建一个 Promise,1 秒后随机出 1~100 的数字,大于 50 就 resolve,否则 reject。用 then/catch 分别处理。
| API | 作用 | 参数 | 返回值 | 代码示例 |
|---|---|---|---|---|
| new Promise(fn) | 造一个"未来才兑现"的约定 | 入参:(resolve,reject)=>{} | 返回:Promise 对象 | let p = new Promise(function (ok, no) { ok(1); }); |
| .then(fn) | 成功之后干啥 | 入参:成功回调 | 返回:新 Promise | p.then(function (v) { console.log(v); }); |
| .catch(fn) | 失败之后干啥 | 入参:失败回调 | 返回:新 Promise | p.catch(function (e) { console.log(e); }); |
| resolve(v) | 把约定成功兑现 | 入参:成功的值 | 状态变成已成功 | resolve("成功了"); |
| reject(e) | 把约定失败拒绝 | 入参:失败原因 | 状态变成已失败 | reject("失败了"); |