源本科技 | 码上会

ECMAScript Promise

2026/04/11
1
0

异步编程与回调地狱

在 JavaScript 中,异步操作是非常常见的场景(如文件读取、网络请求、定时器等)。早期异步编程依赖回调函数实现,当多个异步操作存在依赖关系时,会出现多层嵌套的回调函数,这种代码结构被称为回调地狱

回调地狱的核心弊端:代码嵌套层级深、可读性极差、难以维护和调试。

// 回调地狱示例:多层嵌套定时器
setTimeout(() => {
  console.log("延迟 1 秒输出");

  setTimeout(() => {
    console.log("延迟 2 秒输出");

    setTimeout(() => {
      console.log("延迟 3 秒输出");
    }, 3000);
  }, 2000);
}, 1000);

为了解决回调地狱的问题,ES 6(ECMAScript 2015)中正式推出了 Promise 特性。

核心概念

Promise 是 JavaScript 官方提供的异步编程解决方案,它是一个用于处理异步操作的对象,能够让异步代码的写法更简洁、逻辑更清晰。

三种状态

Promise 实例有且仅有三种状态,状态一旦变更就永久凝固、不可逆转

  1. 待定(pending):初始状态,异步操作正在执行,未得到结果

  2. 已完成(fulfilled):异步操作成功执行,调用 resolve 后触发

  3. 已拒绝(rejected):异步操作执行失败,调用 reject 后触发

状态变更只有两种可能:

  • pendingfulfilled(成功)

  • pendingrejected(失败)

核心 API 特性

  1. Promise 是一个构造函数 通过 new Promise() 创建实例,每个实例对应一个异步操作。

  2. 执行器函数(同步执行) 创建 Promise 时必须传入一个函数,该函数会同步立即执行,内部编写异步逻辑。

  3. 原型方法 .then() 所有 Promise 实例都可以通过原型链调用 .then(),用于指定成功 / 失败的回调。

  4. 原型方法 .catch() / .finally() 用于统一捕获错误,以及定义无论成功失败都执行的逻辑。

基础用法

创建 Promise 实例

Promise 构造函数接收一个执行器函数作为参数,执行器函数有两个形参:

  • resolve:异步操作成功时调用,将状态改为 fulfilled

  • reject:异步操作失败时调用,将状态改为 rejected

// 基础 Promise 示例:封装定时器
const p = new Promise((resolve, reject) => {
  // 异步操作逻辑
  setTimeout(() => {
    // 模拟成功
    resolve("异步操作执行成功");
    // 模拟失败
    // reject(new Error("异步操作执行失败"));
  }, 1000);
});

调用 .then() 处理结果

.then(成功回调, 失败回调) 用于接收异步操作的结果:

  • 成功回调:必选,接收 resolve 传递的参数

  • 失败回调:可选,接收 reject 传递的参数

p.then(
  (result) => { console.log("成功:", result); },
  (error) => { console.log("失败:", error); }
);

then-fs 快速使用

Node.js 原生 fs 模块不支持 Promise 写法,可安装第三方包 then-fs 快速体验 Promise 风格的文件读取:

npm install then-fs

基础读取文件(无序,并行执行):

import thenFs from "then-fs";

thenFs.readFile('./files/1.txt', 'utf8').then(data => console.log(data), err => console.log(err));
thenFs.readFile('./files/2.txt', 'utf8').then(data => console.log(data), err => console.log(err));

链式调用

链式调用原理

.then() 方法默认返回一个新的 Promise 实例,因此可以持续链式调用 .then(),完美解决回调地狱。

按顺序执行异步任务

通过链式调用,保证异步操作按顺序执行(上一个完成后再执行下一个):

import thenFs from "then-fs";

thenFs
  .readFile("./files/1.txt", "utf8")
  .then((data) => {
    console.log(data);
    // 返回新的 Promise 实例
    return thenFs.readFile("./files/2.txt", "utf8");
  })
  .then((data) => {
    console.log(data);
    return thenFs.readFile("./files/3.txt", "utf8");
  })
  .then((data) => {
    console.log(data);
  });

错误处理

异步操作的异常必须通过 Promise 提供的方法捕获,无法使用 try/catch 直接捕获。

全局捕获 .catch()

在链式调用末尾添加 .catch(),可以捕获整个链条中任意位置的错误:

import thenFs from "then-fs";

thenFs
  .readFile("./files/1.txt", "utf8")
  .then((data) => thenFs.readFile("./files/2.txt", "utf8"))
  .then((data) => thenFs.readFile("./files/3.txt", "utf8"))
  .catch((err) => {
    console.log("文件读取失败:", err.message);
  });

提前捕获错误

.catch() 放在链式调用前方,错误不会中断后续 .then() 执行:

thenFs
  .readFile("./files/1.txt", "utf8")
  .catch((err) => console.log("1.txt 读取失败:", err))
  .then((data) => thenFs.readFile("./files/2.txt", "utf8"))
  .then((data) => console.log(data));

最终执行 .finally()

.finally() 是 ES 2018 新增方法,无论异步操作成功或失败,都会执行,常用于清理资源:

p.then(result => {}).catch(err => {}).finally(() => {
  console.log("无论成功失败,我都会执行");
});

静态方法

Promise 构造函数自带 4 个常用静态方法,用于处理多个异步任务

Promise.all()

等待机制:并行执行所有异步操作,全部成功才会触发 .then(),任意一个失败则直接触发 .catch()

import thenFs from "then-fs";

const promiseArr = [
  thenFs.readFile("./files/1.txt", "utf8"),
  thenFs.readFile("./files/2.txt", "utf8"),
];

Promise.all(promiseArr)
  .then(([data1, data2]) => console.log(data1, data2))
  .catch(err => console.log("读取失败:", err));

Promise.race()

赛跑机制:并行执行异步操作,只要有一个完成(成功 / 失败),立即触发对应回调。

Promise.race(promiseArr)
  .then(data => console.log("最先读取完成的文件:", data))
  .catch(err => console.log(err));

Promise.allSettled()

兜底机制:无论成功 / 失败,都会等待所有异步操作执行完毕,返回所有任务的执行结果(无失败情况)。

Promise.any()

成功优先机制:只要有一个异步操作成功,就触发 .then();全部失败才触发 .catch()

自定义 Promise 封装

我们可以基于原生 fs 模块,手动封装支持 Promise 风格的文件读取方法,掌握 Promise 封装的核心逻辑。

完整封装步骤

  1. 定义函数并返回 Promise 实例

  2. 在执行器函数中编写异步逻辑

  3. 根据结果调用 resolve/reject

  4. 导出模块供外部使用

// fileUtil.js
import fs from 'fs';

/**
 * 封装 Promise 风格的文件读取方法
 * @param {string} fPath 文件路径
 * @returns {Promise} Promise 实例
 */
function getFile(fPath) {
  return new Promise((resolve, reject) => {
    // 原生文件读取异步操作
    fs.readFile(fPath, "utf8", (err, data) => {
      if (err) return reject(err);
      resolve(data);
    });
  });
}

// 导出方法
export default { getFile };

使用封装的方法

import fileUtil from "./fileUtil.js";

// 调用封装的 Promise 方法
fileUtil.getFile("./files/1.txt")
  .then(data => console.log("读取成功:", data))
  .catch(err => console.log("读取失败:", err.message));

其他补充

  1. 执行器函数是同步执行的 new Promise(() => {}) 中的函数会立即同步执行,.then() 中的回调是异步执行的。

  2. .catch() 本质 .catch() 等价于 .then(null, 失败回调),是语法糖。

  3. 值穿透特性 如果 .then() 没有传入回调函数,Promise 会自动将结果传递给下一个 .then()

  4. 状态唯一性 一个 Promise 实例的状态只能变更一次,多次调用 resolve/reject 无效。

代码案例

结合所学知识,实现顺序读取文件 + 错误捕获 + 最终清理的完整逻辑:

import fileUtil from "./fileUtil.js";

fileUtil.getFile("./files/1.txt")
  .then(data => {
    console.log(data);
    return fileUtil.getFile("./files/2.txt");
  })
  .then(data => {
    console.log(data);
    return fileUtil.getFile("./files/3.txt");
  })
  .then(data => console.log(data))
  .catch(err => console.log("操作终止:", err.message))
  .finally(() => console.log("文件读取流程执行完毕"));

总结

  1. Promise 核心解决回调地狱,让异步代码扁平化、可维护

  2. 三种状态不可逆,resolve/reject 控制状态变更

  3. 链式调用实现顺序异步操作,.catch() 统一捕获错误

  4. 静态方法 all/race 适用于多异步任务场景

  5. 自定义 Promise 封装是日常开发的核心技能