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 的状态是“一锤定音”的。

注意: Promise 的状态改变是单向的、不可逆的。一旦从 Pending 变为 Fulfilled 或 Rejected,该 Promise 的状态就永久锁定。试着在一个 Promise 里既调用 resolve() 又调用 reject(),只有第一次调用会生效。

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 的魅力。

个人习惯: 我喜欢在每个 .then() 里保持单一职责。如果某个 .then() 里的逻辑太复杂,比如要同时启动两个无关的模块,我会把它拆成两条独立的链,或者使用 Promise.all() 来并行处理。别让单个 .then() 的回调函数超过 10 行,否则可读性会断崖式下降。

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()。

避坑指南: 不要在所有 .then() 后面单独写 .catch()。我建议只在 Promise 链的末尾使用一个统一的 .catch()。如果你需要对特定步骤的错误做精细化处理,可以在链中提前插入 .catch(),但要记得在该 .catch() 的最后,通过 return Promise.reject(newError) 把异常继续向下传递,除非你明确要在这里“吃掉”它并恢复正常流程。

4.4 核心逻辑总览:一张图理清脉络

下面这张图总结了 Promise 从创建到结束的整个生命周期。它展示了状态如何流转,链式调用如何形成,以及错误如何被捕获。

Promise 核心生命周期与链式调用 new Promise(executor) Pending(待定) 调用 resolve(value) 调用 reject(reason) / 抛出错误 Fulfilled(已兑现) Rejected(已拒绝) .then(onFulfilled) .catch(onRejected) 返回新的 Promise 虚线:.then() / .catch() 也返回新 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 把异步操作包装成了一个有状态、可组合、可追溯的对象。它通过链式调用解决了回调地狱的视觉混乱,通过统一的 .catch() 解决了错误处理的散落问题。记住“状态不可逆”和“链式传递值”这两条核心规则,你就能玩转 Promise。

嗯,Promise 的基本用法就是这么回事。下一部分我们会深入聊聊更高级的话题,比如如何让多个 Promise 并行工作,以及如何优雅地处理超时场景。但先把今天这张图和表格记牢,它们是你后面所有理解的基础。