Promise实例方法解析:then()、catch()、finally()

本专栏聚焦Promise的核心原理与高级应用,包含: ✓ Promise A+规范深度解读 ✓ 手写实现与源码分析 ✓ 异步编程设计模式 ✓ 性能调优与错误处理
适合有JavaScript基础,希望深入异步编程的开发者。我们将用最少的篇幅,讲透最核心的知识。

引言:Promise实例方法

在之前的文章中,我们探讨了Promise的状态机模型和不可变原则。理解了Promise的内在状态后,现在让我们转向它的外在行为------实例方法。如果说状态是Promise的"骨骼",那么实例方法就是它的"肌肉",赋予了Promise处理异步操作的能力。

我们可以把Promise就像一台自动咖啡机。状态(pendingfulfilledrejected)是它的指示灯,而then()catch()finally()方法就是它的操作按钮。只有了解每个按钮的确切功能和使用时机,我们才能制作出一杯完美的"异步咖啡"。

then()方法:链式调用的核心

then()的基本语法

Then()方法是为Promise对象添加处理程序的主要方法,也是Promise中最核心、最复杂的方法。它可以接受两个参数:onResolved处理函数onRejected处理函数 。这两个参数都是可选的,如果提供的话,则会在Promise进入 fulfilledrejected 状态时执行,如以下示例:

javascript 复制代码
function onResolved(id) {
    console.log("resolved:" + id)
}

function onRejected(id) {
    console.log("rejected:" + id)
}

let p1 = new Promise((resolve, reject) => resolve())
let p2 = new Promise((resolve, reject) => reject())

p1.then(() => onResolved('p1'),
    () => onRejected('p1'))
p2.then(() => onResolved('p2'),
    () => onRejected('p2'))

上述代码中,由于Promise只能转换一次状态,所以p1和p2这两个操作一定是互斥的,其结果为:

bash 复制代码
resolved:p1
rejected:p2

由于onResolved处理函数onRejected处理函数 这两个参数都是可选的,当我们只想提供 onRejected处理函数 时,可以在 onResolved 的位置上传入undefined或null,这样可以避免在内存中创建多余的对象,因此then()方法在实际使用中,其传参方式有三种:

javascript 复制代码
promise.then(onFulfilled)  // 只处理成功
promise.then(null, onRejected)  // 只处理失败(不推荐)
promise.then(onFulfilled, onRejected)  // 同时处理成功和失败

注:

  • then()方法中,也可以传递一个非函数,如:promise.then('Hello'),但会被静默忽略,因此不推荐这种写法。
  • promise.then(null, onRejected)这种写法也是不推荐的,因为一般默认要处理成功回调。

then()的链式调用

Promise真正的威力来自于then()的链式调用能力,这不仅仅是语法糖,而是一种函数式编程范式的体现。我们来看看下面一个例子:

javascript 复制代码
// 一个典型的链式调用示例
fetchUserData(userId)
  .then(validateUser)            // 1. 验证用户数据
  .then(enrichWithProfile)       // 2. 丰富个人信息
  .then(saveToDatabase)          // 3. 保存到数据库
  .then(notifySubscribers)       // 4. 通知订阅者
  .then(updateCache)             // 5. 更新缓存
  .then(finalizeOperation);      // 6. 完成操作

链式调用的四种返回值模式

返回普通值

当then()方法中返回一个普通值时,该值会被包装为一个新的Promise对象,如以下示例:

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
    resolve('Hello');
})

myPromise.then(value => {
    console.log('收到:', value);
    return '新的普通值';  // 自动包装为 Promise.resolve('新的普通值')
}).then(newValue => {
    console.log('链式传递:', newValue);  // '新的普通值'
});
返回Promise

当then()方法中返回一个Promise对象时,会等待该Promise完成后,继续下一个then()方法的链式调用,如以下示例:

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
    resolve('Hello')
})

myPromise.then(value => {
    return new Promise((resolve, reject) => {
        resolve(value + ' World')
    }) // 返回新的Promise
}).then(response => {
    // 等待new Promise完成
    console.log('处理后的结果:', response)  // Hello World
});
抛出异常

当then()方法中抛出异常时,会转换为一个rejected的Promise对象,如以下示例:

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
    resolve('Hello')
})

myPromise.then(value => {
    if (!value.valid) {
        throw new Error('数据无效')  // 等价于 Promise.reject(new Error('数据无效'))
    }
    return processValue(value)
}).catch(error => {
    console.log('捕获到错误:', error.message)  // '数据无效'
})
没有返回或返回undefined

当then()方法中没有返回值,或返回值为undefined时,会将undefined作为一个值,继续传递,如以下示例:

javascript 复制代码
const myPromise = new Promise((resolve, reject) => {
    resolve('Hello')
})

myPromise.then(value => {
    console.log('处理值:', value)
    // 没有return语句
}).then(nextValue => {
    console.log('下一个值:', nextValue)  // undefined
})

catch()方法:错误处理

catch()的本质

Catch() 用于给Promise添加拒绝处理程序,它只接受一个参数:onRejected处理函数。实际上,catch()方法是一个语法糖,相当于then(null, onRejected)的语法,它的出现极大地提高了代码的可读性。我们来看看下面一个例子:

javascript 复制代码
// 以下两种写法完全等价
promise.catch(onRejected)
promise.then(null, onRejected)

// 但.catch()的可读性更好
fetchData()
  .then(process)
  .catch(handleError)  // 清晰:这里处理错误
  .then(continueAfterError)

// 对比:使用.then()的第二个参数
fetchData()
  .then(process, handleError)  // 不清晰:是处理fetch错误还是process错误?
  .then(continueAfterError)

错误传播机制

Promise的错误处理遵循冒泡原则:错误会沿着Promise链向后传递,直到被捕获,如以下示例:

javascript 复制代码
// 错误传播示例
Promise.resolve()
    .then(() => {
        console.log('步骤1: 成功')
        return '第一步结果'
    })
    .then(result => {
        console.log('步骤2: 收到', result)
        throw new Error('步骤2发生错误')  // 抛出错误
    })
    .then(result => {
        console.log('步骤3: 这行不会执行')  // 被跳过
        return '第三步结果';
    })
    .catch(error => {
        console.log('捕获到错误:', error.message)  // '步骤2发生错误'
        return '从错误中恢复';
    })
    .then(recovery => {
        console.log('恢复后继续:', recovery)  // '从错误中恢复'
    });

finally()方法:资源清理

finally()方法的独特性

Finally()方法用于给Promise实例添加 onFinally处理程序,这个方法可以避免onResolved处理函数onRejected处理函数 中出现冗余代码,无论Promise状态为 fulfilledrejected 的都会执行,即无论Promise是成功还是失败,它都会执行。我们来看看下面的例子:

javascript 复制代码
// .finally()的基本使用
fetchData()
    .then(data => {
        console.log('数据处理:', data);
        return process(data);
    })
    .catch(error => {
        console.error('处理失败:', error);
        throw error; // 重新抛出,让外部知道失败
    })
    .finally(() => {
        console.log('清理资源');  // 无论成功失败都会执行
        cleanupResources();
    });

finally()方法的三个特性

不接受参数

javascript 复制代码
promise.finally(() => {
    // 这里无法访问Promise的结果或错误原因
    console.log('执行清理')
})

原样传递上游的结果或错误

javascript 复制代码
Promise.resolve('成功数据')
    .finally(() => {
        console.log('finally执行');
        // 这里返回的值不会影响链的传递
        return 'finally的返回值会被忽略';
    })
    .then(value => {
        console.log('收到:', value);  // '成功数据',不是'finally的返回值'
    })

如果finally抛出错误,会覆盖之前的错误

javascript 复制代码
Promise.resolve('原始数据')
    .finally(() => {
        throw new Error('finally中的错误');  // 这个错误会覆盖之前的结果
    })
    .then(value => {
        console.log('这不会执行');  // 被跳过
    })
    .catch(error => {
        console.log('捕获的错误:', error.message);  // 'finally中的错误'
    });

总结

方法对比总结

方法 主要用途 返回值影响 执行时机 最佳实践
then() 处理成功结果,链式传递 决定下一环的输入 前一个Promise完成后 保持纯函数,明确返回值
catch() 错误捕获和恢复 可恢复错误或重新抛出 链中任何错误发生时 在适当层级处理,不要过早吞没错误
finally() 资源清理和状态重置 不影响结果传递 无论如何都会执行 只做清理,不返回业务数据

黄金法则

  • 单一职责原则:每个then()应该只做一件事。
  • 错误早抛,晚处理:让错误传播到合适的处理层。
  • finally()只清理:不要在finally中返回业务逻辑数据。
  • 保持链的可读性:合理拆分长链,使用命名函数。
  • 考虑可维护性:为复杂的链添加注释和文档。

结语

本文主要介绍了Promise的三种实例化方法,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!

相关推荐
踢球的打工仔12 小时前
typescript-var和let作用域
前端·javascript·typescript
手握风云-12 小时前
JavaEE 进阶第八期:Spring MVC - Web开发的“交通枢纽”(二)
前端·spring·java-ee
海云前端112 小时前
前端组件封装封神指南:16条实战原则,面试、项目双加分
前端
C_心欲无痕12 小时前
网络相关 - XSS跨站脚本攻击与防御
前端·网络·xss
2501_9418752812 小时前
从日志语义到可观测性的互联网工程表达升级与多语言实践分享随笔
java·前端·python
钰fly12 小时前
DataGridView 与 DataTable 与csv 序列
前端·c#
龙在天13 小时前
Nuxtjs中,举例子一篇文章讲清楚:水合sop
前端·nuxt.js
holidaypenguin13 小时前
【转】跨浏览器 Canvas 图像解码终极方案:让大图渲染也能丝滑不卡顿
前端·canvas
狗哥哥13 小时前
企业级 Vue3 + Element Plus 主题定制架构:从“能用”到“好用”的进阶之路
前端·css·架构
星辰引路-Lefan13 小时前
[特殊字符] 开源一款基于 PaddleOCR 的纯离线 OCR 识别插件 | 支持身份证、银行卡、驾驶证识别
前端·开源·ocr