JavaScript学习笔记:13.Promise
上一篇咱们用“设计图纸”搞定了类的封装与继承,这一篇要攻克JS开发的“异步老大难”——Promise。做前端绕不开异步:请求接口要等服务器响应、加载图片要等资源下载、定时器要等时间触发…… 而在Promise出现前,咱们只能用“回调函数”处理这些操作,写出来的代码常常像“俄罗斯套娃”——一层嵌一层,这就是让人头秃的“回调地狱”。
Promise的出现,就像给异步任务发了一封“正式承诺函”:它明确告诉程序“这个任务要么成功给你结果,要么失败给你原因,在此之前你该干啥干啥”。今天咱们就从“为什么需要Promise”讲起,用“餐厅点餐”的生活化比喻,把Promise的状态机制、链式调用、静态方法和实战技巧彻底讲透,让你再也不用面对嵌套的回调皱眉头。
一、先破案:为什么需要Promise?回调地狱有多坑?
先看一个场景:先点奶茶(异步),奶茶做好后点蛋糕(异步),蛋糕做好后打包带走(异步)。用传统回调函数写是这样的:
// 回调地狱:一层套一层,代码向右偏移,维护性极差
orderMilkTea("珍珠奶茶", function(milkTea) {
console.log(`拿到${milkTea}`);
orderCake("芝士蛋糕", function(cake) {
console.log(`拿到${cake}`);
packFood([milkTea, cake], function(packed) {
console.log(`打包完成:${packed}`);
// 再嵌套更多异步操作,代码就彻底乱了
}, function(err) {
console.log("打包失败", err);
});
}, function(err) {
console.log("点蛋糕失败", err);
});
}, function(err) {
console.log("点奶茶失败", err);
});
这种代码的问题显而易见:
- 嵌套层级深:像爬楼梯一样,越往后越靠右,可读性差;
- 错误处理分散:每个回调都要单独写错误处理,重复且冗余;
- 无法并行/中断:多个异步任务只能串行,想并行处理或中途中断非常麻烦。
而用Promise改写后,代码瞬间“站直了”:
// Promise链式调用:平级代码,清晰易懂
orderMilkTea("珍珠奶茶")
.then(milkTea => {
console.log(`拿到${milkTea}`);
return orderCake("芝士蛋糕"); // 返回新的Promise,衔接下一个任务
})
.then(cake => {
console.log(`拿到${cake}`);
return packFood([milkTea, cake]);
})
.then(packed => console.log(`打包完成:${packed}`))
.catch(err => console.log("流程失败", err)); // 统一错误处理
💡 核心差异:Promise把“嵌套的回调”变成了“链式的then”,错误处理也统一交给最后一个catch,代码结构瞬间清晰。这就是Promise的核心价值——规范化异步流程,解决回调地狱。
二、Promise核心概念:一张“承诺函”的三个状态
Promise翻译过来是“承诺”,它的核心就是用“状态”来描述异步任务的执行结果。可以把Promise想象成“餐厅点餐单”:
- pending(等待中):订单已提交,厨师正在做,任务还没完成;
- fulfilled(已成功):餐做好了,任务成功完成,拿到结果;
- rejected(已失败):食材用完了,任务失败,拿到错误原因。
这三个状态有两个铁律,是理解Promise的关键:
- 状态不可逆:一旦从pending变成fulfilled或rejected,就再也变不回去了——就像餐做好了不能再变回制作中,没食材了也不能再上菜;
- 结果唯一:成功时只有一个“结果值(value)”,失败时只有一个“原因(reason)”,不会出现既成功又失败的情况。
三、Promise基础用法:从“发承诺”到“收结果”
Promise的用法就像“点餐→等餐→用餐”,核心分三步:创建Promise、改变状态、处理结果。
1. 第一步:创建Promise(提交订单)
用new Promise()创建Promise实例,接收一个“执行器函数”作为参数,这个函数有两个内置参数:resolve(成功时调用,相当于“上菜”)和reject(失败时调用,相当于“告知没餐”)。
// 封装一个“点奶茶”的Promise函数
function orderMilkTea(type) {
// 执行器函数:立即执行,里面包含异步任务
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟异步操作:1秒后完成
const hasIngredient = Math.random() > 0.3; // 70%概率有食材
if (hasIngredient) {
// 成功:调用resolve,传递结果
resolve(`${type}(少糖少冰)`);
} else {
// 失败:调用reject,传递错误原因
reject(new Error(`点${type}失败:珍珠卖完了`));
}
}, 1000);
});
}
📌 注意:执行器函数会立即执行,不是等到调用then才执行——就像提交订单后,厨师立刻开始做,不是等你催单才动手。
2. 第二步:处理结果(用餐/处理没餐)
用then()处理成功结果,用catch()处理失败结果,还能加finally()处理“无论成功失败都要做的事”(比如收起点餐单)。
// 调用Promise函数,处理结果
orderMilkTea("珍珠奶茶")
.then(result => {
// 成功回调:接收resolve的结果
console.log(`用餐:${result}`);
})
.catch(error => {
// 失败回调:接收reject的原因
console.log(`处理失败:${error.message}`);
})
.finally(() => {
// 无论成功失败,都会执行
console.log("流程结束:收起点餐单");
});
执行结果(二选一):
// 成功情况
用餐:珍珠奶茶(少糖少冰)
流程结束:收起点餐单
// 失败情况
处理失败:点珍珠奶茶失败:珍珠卖完了
流程结束:收起点餐单
四、核心技巧:链式调用——异步任务的“流水线”
Promise的灵魂是“链式调用”,then()会返回一个新的Promise,让多个异步任务像流水线一样依次执行,而不是嵌套。
1. 链式调用的核心逻辑
- 每个
then()的返回值,会成为下一个then()的参数; - 如果
then()返回的是Promise,下一个then()会等待这个Promise完成,再接收结果; - 任何一个环节抛出错误,都会跳过后续
then(),直接进入最近的catch()。
// 流水线示例:点奶茶→点蛋糕→打包
orderMilkTea("珍珠奶茶")
.then(milkTea => {
console.log(`拿到奶茶:${milkTea}`);
return orderCake("芝士蛋糕"); // 返回新Promise,衔接下一个任务
})
.then(cake => {
console.log(`拿到蛋糕:${cake}`);
return packFood([milkTea, cake]); // 继续返回Promise
})
.then(packed => console.log(`打包完成:${packed}`))
.catch(err => console.log(`流程失败:${err.message}`));
2. 常见坑:忘记return导致“漂浮Promise”
如果then()里启动了异步任务,但没返回它,这个任务就会“漂浮”——无法追踪状态,下一个then()会提前执行。
// 反面例子:忘记return,fetch是漂浮Promise
orderMilkTea("珍珠奶茶")
.then(milkTea => {
fetch("/api/recordOrder"); // 没返回,无法追踪结果
})
.then(() => {
console.log("订单记录完成"); // 可能在fetch完成前执行
});
// 正面例子:返回Promise,确保顺序
orderMilkTea("珍珠奶茶")
.then(milkTea => {
return fetch("/api/recordOrder"); // 返回Promise,下一个then等待它完成
})
.then(() => {
console.log("订单记录完成"); // 正确:fetch完成后执行
});
五、错误处理:Promise的“安全网”
Promise的错误处理就像“餐厅的投诉渠道”,能统一捕获整个链条的错误,还支持细粒度处理。
1. 统一错误处理(全局投诉)
用链条末尾的catch(),捕获所有环节的错误(包括then()里抛出的异常):
orderMilkTea("珍珠奶茶")
.then(milkTea => {
if (milkTea.includes("珍珠")) {
throw new Error("我不爱喝珍珠,换椰果"); // 手动抛出错误
}
return orderCake("芝士蛋糕");
})
.then(cake => packFood([milkTea, cake]))
.catch(err => console.log(`统一处理错误:${err.message}`));
// 输出:统一处理错误:我不爱喝珍珠,换椰果
2. 细粒度错误处理(局部投诉)
嵌套then()里的catch(),只处理当前环节的错误,不影响后续流程:
orderMilkTea("珍珠奶茶")
.then(milkTea => {
// 可选环节:加配料(失败不影响主流程)
return addTopping(milkTea, "椰果")
.catch(err => {
console.log(`加配料失败:${err.message}`);
return milkTea; // 返回原奶茶,流程继续
});
})
.then(milkTea => orderCake("芝士蛋糕"))
.then(cake => packFood([milkTea, cake]))
.catch(err => console.log(`主流程失败:${err.message}`));
3. catch后的链式恢复
catch()后还能继续接then(),实现“错误恢复”——就像投诉后餐厅补救,流程继续:
orderMilkTea("珍珠奶茶")
.then(milkTea => {
throw new Error("奶茶太甜了");
})
.catch(err => {
console.log(`处理错误:${err.message},换无糖的`);
return orderMilkTea("珍珠奶茶(无糖)"); // 补救,返回新Promise
})
.then(milkTea => console.log(`最终拿到:${milkTea}`))
.finally(() => console.log("流程结束"));
六、静态方法:Promise的“团队协作模式”
Promise提供了四个静态方法,处理多个异步任务的协作,就像“餐厅处理多个订单”的不同策略。
| 方法 | 作用 | 核心特点 | 场景示例 |
|---|---|---|---|
Promise.all([p1,p2,p3]) | 所有任务成功才成功,一个失败就失败 | 快速失败,结果按顺序返回 | 同时点奶茶和蛋糕,都做好才打包 |
Promise.allSettled([p1,p2,p3]) | 等待所有任务完成,无论成功失败 | 全部 settle,返回每个任务的状态和结果 | 统计多个接口的请求结果(成功/失败都要知道) |
Promise.any([p1,p2,p3]) | 任意一个任务成功就成功,全部失败才失败 | 快速成功,返回第一个成功的结果 | 多个CDN加载图片,哪个快用哪个 |
Promise.race([p1,p2,p3]) | 任意一个任务 settle(成功/失败)就结束 | 先到先得,返回第一个 settle 的结果 | 接口请求超时控制(请求和定时器赛跑) |
实战示例:
// 1. Promise.all:全部成功才返回
const p1 = orderMilkTea("珍珠奶茶");
const p2 = orderCake("芝士蛋糕");
Promise.all([p1, p2])
.then([milkTea, cake] => packFood([milkTea, cake]))
.then(packed => console.log(`打包完成:${packed}`))
.catch(err => console.log(`有订单失败:${err.message}`));
// 2. Promise.race:超时控制
const request = fetch("/api/data");
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error("请求超时")), 5000);
});
Promise.race([request, timeout])
.then(data => console.log("请求成功", data))
.catch(err => console.log("请求失败", err.message));
七、async/await:Promise的“语法糖”,写异步像写同步
async/await是ES2017新增的语法,基于Promise,让异步代码看起来像同步代码,更易读。
1. 基础用法:
async修饰函数,函数返回值自动变成Promise;await只能在async函数内使用,等待Promise完成并返回结果;- 错误用
try/catch捕获,和同步代码的错误处理一致。
// 用async/await改写之前的流水线
async function orderMeal() {
try {
// 等待奶茶做好
const milkTea = await orderMilkTea("珍珠奶茶");
console.log(`拿到奶茶:${milkTea}`);
// 等待蛋糕做好
const cake = await orderCake("芝士蛋糕");
console.log(`拿到蛋糕:${cake}`);
// 等待打包
const packed = await packFood([milkTea, cake]);
console.log(`打包完成:${packed}`);
return packed;
} catch (err) {
console.log(`流程失败:${err.message}`);
} finally {
console.log("流程结束:收起点餐单");
}
}
// 调用async函数(返回Promise)
orderMeal();
2. 并发处理:await Promise.all
await 配合Promise.all,能实现并发异步任务:
async function orderBatch() {
try {
// 同时发起两个请求,并发执行
const [milkTea, cake] = await Promise.all([
orderMilkTea("珍珠奶茶"),
orderCake("芝士蛋糕")
]);
console.log(`同时拿到:${milkTea}、${cake}`);
} catch (err) {
console.log(`失败:${err.message}`);
}
}
八、避坑指南:Promise的“常见陷阱”
- 状态不可逆:一旦resolve/reject,后续再调用resolve/reject无效;
- 忘记return Promise:导致链条断裂,下一个then提前执行;
- 错误吞噬:嵌套then里的错误,若没catch,会被外层catch捕获,但可能掩盖真正的错误位置;
- 同步错误:执行器函数里的同步错误,会直接导致Promise reject,需用try/catch捕获;
- 微任务时序:then回调是微任务,会在当前同步代码执行完后、下一轮事件循环前执行(和setTimeout的任务队列不同)。
// 微任务时序示例
console.log("1. 同步代码开始");
Promise.resolve().then(() => console.log("2. Promise then(微任务)"));
setTimeout(() => console.log("3. setTimeout(任务队列)"), 0);
console.log("4. 同步代码结束");
// 输出顺序:1 → 4 → 2 → 3
九、总结:Promise的核心价值
Promise的本质是“异步任务的规范化管理工具”,它解决了回调地狱的痛点,提供了清晰的链式流程和统一的错误处理,而async/await让它的用法更接近同步代码。
核心价值总结:
- 把“嵌套回调”变成“扁平链式”,可读性提升;
- 统一错误处理,避免重复代码;
- 支持多个异步任务的协作(并发、竞速等);
- 为async/await打下基础,简化异步编程。
Promise是JS异步编程的基石,掌握它的状态机制、链式调用和静态方法,能让你从容应对接口请求、资源加载等各种异步场景。
1657

被折叠的 条评论
为什么被折叠?



