探索 Node.js 中的 Promise

Promise 是 JavaScript 中的一个特殊对象,表示一个异步操作的最终完成(或失败)及其结果值。本质上,Promise 是一个尚未可用但将来会有的值的占位符。

把 Promise 想象成点披萨:你不会立刻拿到它,但外卖员承诺稍后会送到。你不知道*确切*的时间,但你知道结果要么是“披萨已送达”,要么是“出了点问题”。

Promise 的状态

一个 Promise 可以处于以下三种状态之一:

  • 待定(Pending):初始状态,异步操作仍在进行中。
  • 已兑现(Fulfilled):操作成功完成,Promise 现在已用一个值解决(resolved)。
  • 已拒绝(Rejected):操作失败,Promise 因一个原因(通常是错误)而敲定(settled)。

当你点披萨时,你处于待定状态,饥饿而充满希望。如果披萨热腾腾、香喷喷地送达,你就进入了已兑现状态。但如果餐厅打电话说他们把你的披萨掉地上了,你就处于已拒绝状态。

无论你的晚餐是喜是悲,一旦有了最终结果,Promise 就被认为是已敲定(settled)

Promise 的基本语法

创建 Promise 最常见的方法之一是使用 new Promise() 构造函数。该构造函数接受一个带有两个参数的函数:resolvereject。这两个函数用于将 Promise 从待定状态转换到已兑现已拒绝状态。

如果在执行器函数内部抛出错误,Promise 将因该错误而被拒绝。执行器函数的返回值会被忽略:只应使用 resolvereject 来敲定 Promise。

const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});

在上面的例子中:

  • 如果 success 条件为 true,Promise 将被兑现,并将值 'Operation was successful!' 传递给 resolve 函数。
  • 如果 success 条件为 false,Promise 将被拒绝,并将错误 'Something went wrong.' 传递给 reject 函数。

使用 .then().catch().finally() 处理 Promise

一旦创建了 Promise,你就可以使用 .then().catch().finally() 方法来处理其结果。

  • .then() 用于处理已兑现的 Promise 并访问其结果。
  • .catch() 用于处理已拒绝的 Promise 并捕获可能发生的任何错误。
  • .finally() 用于处理已敲定的 Promise,无论 Promise 是解决还是拒绝。
const  = new ((, ) => {
  const  = true;

  if () {
    ('Operation was successful!');
  } else {
    ('Something went wrong.');
  }
});


  .( => {
    .(); // This will run if the Promise is fulfilled
  })
  .( => {
    .(); // This will run if the Promise is rejected
  })
  .(() => {
    .('The promise has completed'); // This will run when the Promise is settled
  });

链式调用 Promise

Promise 的一个强大特性是它们允许你将多个异步操作链接在一起。当你链式调用 Promise 时,每个 .then() 块都会等待前一个块完成后再运行。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'First task completed');


  .( => {
    .(); // 'First task completed'
    return (1000).(() => 'Second task completed'); // Return a second Promise
  })
  .( => {
    .(); // 'Second task completed'
  })
  .( => {
    .(); // If any Promise is rejected, catch the error
  });

将 Async/Await 与 Promise 结合使用

在现代 JavaScript 中,处理 Promise 的最佳方式之一是使用 async/await。这让你能够编写看起来像同步代码的异步代码,使其更易于阅读和维护。

  • async 用于定义一个返回 Promise 的函数。
  • await 用于在 async 函数内部暂停执行,直到一个 Promise 敲定。
async function () {
  try {
    const  = await promise1;
    .(); // 'First task completed'

    const  = await promise2;
    .(); // 'Second task completed'
  } catch () {
    .(); // Catches any rejection or error
  }
}

();

performTasks 函数中,await 关键字确保每个 Promise 在继续执行下一条语句之前都已敲定。这使得异步代码的流程更加线性和易读。

本质上,上述代码的执行效果与用户编写以下代码相同:

promise1
  .then(function () {
    .();
    return promise2;
  })
  .then(function () {
    .();
  })
  .catch(function () {
    .();
  });

顶层 Await

使用 ECMAScript 模块时,模块本身被视为一个原生支持异步操作的顶层作用域。这意味着你可以在顶层使用 await,而无需 async 函数。

import {  as  } from 'node:timers/promises';

await (1000);

Async/await 的用法可能比所提供的简单示例复杂得多。Node.js 技术指导委员会成员 James Snell 有一个深入的演讲,探讨了 Promise 和 async/await 的复杂性。

基于 Promise 的 Node.js API

Node.js 为其许多核心 API 提供了基于 Promise 的版本,特别是在传统上使用回调处理异步操作的情况下。这使得使用 Node.js API 和 Promise 更加容易,并降低了“回调地狱”的风险。

例如,fs(文件系统)模块在 fs.promises 下有一个基于 Promise 的 API:

const  = ('node:fs').;
// Or, you can import the promisified version directly:
// const fs = require('node:fs/promises');

async function () {
  try {
    const  = await .('example.txt', 'utf8');
    .();
  } catch () {
    .('Error reading file:', );
  }
}

();

在这个例子中,fs.readFile() 返回一个 Promise,我们使用 async/await 语法来异步读取文件内容。

高级 Promise 方法

JavaScript 的 Promise 全局对象提供了几个强大的方法,可以帮助更有效地管理多个异步任务:

Promise.all()

此方法接受一个 Promise 数组,并返回一个新的 Promise。这个新的 Promise 会在所有 Promise 都兑现后解决。如果任何一个 Promise 被拒绝,Promise.all() 会立即拒绝。然而,即使发生拒绝,其他 Promise 仍会继续执行。在处理大量 Promise 时,尤其是在批处理中,使用此函数可能会对系统内存造成压力。

const { :  } = ('node:timers/promises');

const  = (1000).(() => 'Data from API 1');
const  = (2000).(() => 'Data from API 2');

.([, ])
  .( => {
    .(); // ['Data from API 1', 'Data from API 2']
  })
  .( => {
    .('Error:', );
  });

Promise.allSettled()

此方法等待所有 promise 都解决或拒绝,并返回一个对象数组,描述每个 Promise 的结果。

const  = .('Success');
const  = .('Failed');

.([, ]).( => {
  .();
  // [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});

Promise.all() 不同,Promise.allSettled() 不会在失败时短路。它会等待所有 promise 都敲定,即使有些被拒绝。这为批处理操作提供了更好的错误处理,因为你可能想知道所有任务的状态,无论成功与否。

Promise.race()

此方法在第一个 Promise 敲定(无论是解决还是拒绝)时立即解决或拒绝。无论哪个 promise 先敲定,所有 promise 都会被完全执行。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'Task 1 done');
const  = (1000).(() => 'Task 2 done');

.([, ]).( => {
  .(); // 'Task 2 done' (since task2 finishes first)
});

Promise.any()

此方法在任意一个 Promise 解决后立即解决。如果所有 promise 都被拒绝,它将以一个 AggregateError 拒绝。

const { :  } = ('node:timers/promises');

const  = (2000).(() => 'API 1 success');
const  = (1000).(() => 'API 2 success');
const  = (1500).(() => 'API 3 success');

.([, , ])
  .( => {
    .(); // 'API 2 success' (since it resolves first)
  })
  .( => {
    .('All promises rejected:', );
  });

Promise.reject()Promise.resolve()

这些方法直接创建一个已拒绝或已解决的 Promise。

.('Resolved immediately').( => {
  .(); // 'Resolved immediately'
});

Promise.try()

Promise.try() 是一个执行给定函数的方法,无论该函数是同步还是异步,并将其结果包装在一个 promise 中。如果函数抛出错误或返回一个被拒绝的 promise,Promise.try() 将返回一个被拒绝的 promise。如果函数成功完成,返回的 promise 将以其值兑现。

这对于以一致的方式启动 promise 链特别有用,尤其是在处理可能同步抛出错误的代码时。

function () {
  if (.() > 0.5) {
    throw new ('Oops, something went wrong!');
  }
  return 'Success!';
}

.()
  .( => {
    .('Result:', );
  })
  .( => {
    .('Caught error:', .message);
  });

在这个例子中,Promise.try() 确保如果 mightThrow() 抛出错误,它将在 .catch() 块中被捕获,从而更容易地在一个地方处理同步和异步错误。

Promise.withResolvers()

此方法创建一个新的 promise 及其关联的 resolve 和 reject 函数,并将它们返回在一个方便的对象中。例如,当你需要创建一个 promise,但稍后从执行器函数外部解决或拒绝它时,可以使用此方法。

const { , ,  } = .();

(() => {
  ('Resolved successfully!');
}, 1000);

.( => {
  .('Success:', );
});

在此示例中,Promise.withResolvers() 让你完全控制 promise 何时以及如何被解决或拒绝,而无需内联定义执行器函数。这种模式常用于事件驱动编程、超时或与非基于 promise 的 API 集成时。

使用 Promise 进行错误处理

处理 Promise 中的错误可确保你的应用程序在出现意外情况时能正确运行。

  • 你可以使用 .catch() 来处理在 Promise 执行期间发生的任何错误或拒绝。
myPromise
  .then( => .())
  .catch( => .()) // Handles the rejection
  .finally( => .('Promise completed')); // Runs regardless of promise resolution
  • 或者,在使用 async/await 时,你可以使用 try/catch 块来捕获和处理错误。
async function () {
  try {
    const  = await myPromise;
    .();
  } catch () {
    .(); // Handles any errors
  } finally {
    // This code is executed regardless of failure
    .('performTask() completed');
  }
}

();

在事件循环中调度任务

除了 Promise,Node.js 还提供了几种其他机制来在事件循环中调度任务。

queueMicrotask()

queueMicrotask() 用于调度一个微任务,这是一个轻量级任务,在当前执行的脚本之后、任何其他 I/O 事件或计时器之前运行。微任务包括 Promise 解决和其他优先于常规任务的异步操作。

(() => {
  .('Microtask is executed');
});

.('Synchronous task is executed');

在上面的例子中,“Microtask is executed” 将在 “Synchronous task is executed” 之后、任何 I/O 操作(如计时器)之前被打印。

process.nextTick()

process.nextTick() 用于调度一个回调,在当前操作完成后立即执行。这对于希望确保回调尽快执行,但仍需在当前执行上下文之后的情况非常有用。

.(() => {
  .('Next tick callback');
});

.('Synchronous task executed');

setImmediate()

setImmediate() 调度一个回调,在 Node.js 事件循环的检查阶段执行,该阶段在轮询阶段之后运行,大多数 I/O 回调都在轮询阶段处理。

(() => {
  .('Immediate callback');
});

.('Synchronous task executed');

何时使用它们

  • 对于需要在当前脚本之后、任何 I/O 或计时器回调之前立即运行的任务(通常用于 Promise 解决),请使用 queueMicrotask()
  • 对于应在任何 I/O 事件之前执行的任务(通常用于延迟操作或同步处理错误),请使用 process.nextTick()
  • 对于应在轮询阶段之后、大多数 I/O 回调处理完毕后运行的任务,请使用 setImmediate()

因为这些任务在当前的同步流程之外执行,所以这些回调中的未捕获异常不会被周围的 try/catch 块捕获,并且如果管理不当(例如,通过给 Promise 附加 .catch() 或使用全局错误处理器如 process.on('uncaughtException')),可能会导致应用程序崩溃。

有关事件循环以及各阶段执行顺序的更多信息,请参阅相关文章 Node.js 事件循环