4. Promise 模式:状态、链式调用与错误处理
好,我们来聊聊 Promise。说实话,在接触回调地狱之后,我第一次看到 Promise 时,感觉就像在乱糟糟的接线盒里找到了一根标签清晰的排线。它不是一个全新的概念,而是一种更优雅的组织方式。
Promise 的核心价值在于:它把异步操作的未来结果抽象成了一个对象。你可以像操作普通变量一样,对这个“未来结果”进行组合、传递和错误处理。这不仅仅是语法糖,而是思维模式的转变。
4.1 三种状态:一锤定音
Promise 对象在生命周期里只会处于三种状态之一:
| 状态 | 含义 | 是否可转移 |
|---|---|---|
| Pending(待定) | 初始状态。操作尚未完成,也没有被拒绝。 | 是(可转为 Fulfilled 或 Rejected) |
| Fulfilled(已兑现) | 操作成功完成。Promise 有了一个结果值。 | 否(已定型) |
| Rejected(已拒绝) | 操作失败。Promise 有了一个拒绝原因(通常是 Error 对象)。 | 否(已定型) |
这里有个关键点:状态一旦定型(settled),就不可逆。它要么变成 Fulfilled,要么变成 Rejected。我曾经在项目中看到有人试图在 .then() 里重新 resolve 同一个 Promise,结果自然是徒劳的。这不像你写一个变量,今天赋值为 1,明天还能改成 2。Promise 的状态是“一锤定音”的。
4.2 链式调用:扁平化异步流程
链式调用是 Promise 最直观的威力展示。它解决了回调地狱的缩进爆炸问题。
每个 .then() 或 .catch() 都会返回一个新的 Promise。这个新 Promise 的状态由回调函数的返回值决定:
- 如果回调函数返回一个值(非 Promise),新 Promise 会立即以该值落定(resolved)。
- 如果回调函数返回另一个 Promise,新 Promise 会“跟随”那个 Promise 的状态。
- 如果回调函数抛出一个异常,新 Promise 会以该异常拒绝(rejected)。
我习惯把 Promise 链想象成一条传送带。每个 .then() 就是一个工位,前一个工位处理完,把结果送到下一个工位。这个过程在代码里看起来是垂直的,逻辑却是线性的。
举个我们在 SDK 集成中常见的例子:
// 异步获取用户配置,然后根据配置初始化模块
// 错误示范:回调嵌套
getUserConfig(function(config) {
initModuleA(config.moduleA, function(resultA) {
initModuleB(config.moduleB, function(resultB) {
// 越来越深...
}, function(err) {
// 处理 initModuleB 错误
});
}, function(err) {
// 处理 initModuleA 错误
});
}, function(err) {
// 处理 getUserConfig 错误
});
// Promise 链式调用正确示范
getUserConfig()
.then(config => {
// 返回一个新 Promise,形成一个新工位
return initModuleA(config.moduleA);
})
.then(resultA => {
// resultA 来自 initModuleA 的 resolve
// 但是我们现在需要 moduleB 的配置,怎么办?
// 记住,.then() 里可以访问外部作用域的 config
// 或者更好的方式:在第一个 .then() 里返回一个包含所有需要信息的对象
return { resultA, config };
})
.then(({ resultA, config }) => {
return initModuleB(config.moduleB);
})
.then(resultB => {
console.log('所有模块初始化完成:', resultA, resultB);
})
.catch(err => {
// 统一错误处理
console.error('初始化流程失败:', err);
});
你看,虽然步骤变多了,但每一层只做一件事,错误处理也被集中到了末尾。这就是 Promise 的魅力。
4.3 错误处理:错必究,但不蔓延
Promise 的错误处理有两个核心机制:.catch() 和 throw。
- 冒泡式捕获: 错误会沿着 Promise 链向下传递,直到被最近的
.catch()捕获。这有点像 try-catch 的行为。 - 中断链式传递: 如果
.catch()没有返回一个新的 Promise 或抛出一个错误,那么它之后的.then()仍然会正常执行。这很方便,但也要小心。
我曾经在调试一个支付 SDK 的集成问题时,花了大半天才找到原因:
// 一个隐蔽的错误处理陷阱
doSomething()
.then(result => {
// 假设这步抛出一个网络错误
throw new Error('网络超时');
})
.catch(err => {
// 捕获错误,做了日志记录
console.error('发生了错误:', err);
// 但是!这里没有返回任何东西,也没有抛出新的错误
// 所以这个 catch 返回了一个 resolved 的 Promise(值为 undefined)
})
.then(() => {
// 这个 .then() 会正常执行!
// 但它收到的值是 undefined,而不是我们期望的数据
// 如果后续代码依赖上一步的结果,就会产生难以追踪的 bug
console.log('居然还能走到这里...');
});
这个问题让我养成了一个习惯:在 .catch() 里,如果你处理了错误并且流程不应该继续,一定要 重新抛出错误,或者返回一个 Promise.reject()。
return Promise.reject(newError) 把异常继续向下传递,除非你明确要在这里“吃掉”它并恢复正常流程。
4.4 核心逻辑总览:一张图理清脉络
下面这张图总结了 Promise 从创建到结束的整个生命周期。它展示了状态如何流转,链式调用如何形成,以及错误如何被捕获。
这张图的核心信息是:
- Promise 从 Pending 开始,只能进入 Fulfilled 或 Rejected 其中之一。
- 进入 Fulfilled 后,会触发
.then()里的回调。 - 进入 Rejected 后,会触发
.catch()里的回调。 - 无论是
.then()还是.catch(),它们本身都会返回一个新的 Promise,这就是链式调用的基础。
熟悉这张图,你就能看懂任何基于 Promise 的异步逻辑。
4.5 归纳总结:一张表读懂 Promise
为了方便你以后快速查阅,我把 Promise 的核心要点整理成了表格:
| 特性 | 说明 | 常见误区 |
|---|---|---|
| 状态不可逆 | Pending -> Fulfilled 或 Rejected,定型后无法更改。 | 试图在 .then() 里修改父级 Promise 的状态。 |
| 链式传递值 | .then() 的回调返回值会成为下一个 .then() 的输入。 | .then() 的回调忘记显式 return,导致后续收到 undefined。 |
| 错误冒泡 | Rejected 状态会沿着链条向下,直到被 .catch() 处理。 | .catch() 处理完错误后未重新抛出,导致“错误被静默消化”。 |
| 异常安全 | 在 .then() 或 executor 里 throw 错误,Promise 会自动转为 Rejected。 | 认为 Promise 里的 try-catch 和普通的 try-catch 完全等价(其实基本等价,但要注意回调是异步的)。 |
嗯,Promise 的基本用法就是这么回事。下一部分我们会深入聊聊更高级的话题,比如如何让多个 Promise 并行工作,以及如何优雅地处理超时场景。但先把今天这张图和表格记牢,它们是你后面所有理解的基础。