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