探索 Node.js 中的 Promise

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

将 Promise 想象成订购披萨:您不会立即拿到,但送货员承诺稍后会将其带给您。您不确切知道何时,但您知道结果要么是“披萨已送达”,要么是“出了点问题”。

Promise 状态

Promise 可以处于三种状态之一

  • Pending:初始状态,其中异步操作仍在运行。
  • Fulfilled:操作成功完成,并且 Promise 现在已使用值解析。
  • Rejected:操作失败,并且 Promise 已使用原因(通常是错误)确定。

当您订购披萨时,您处于 pending 状态,饥饿且充满希望。如果披萨又热又香,您就进入了 fulfilled 状态。但是,如果餐厅打电话说他们把您的披萨掉在地板上,那么您就处于 rejected 状态。

无论您的晚餐最终是快乐还是失望,一旦有了最终结果,Promise 就会被认为是settled(已完成)。

Promise 的基本语法

创建 Promise 的最常见方法之一是使用 new Promise() 构造函数。该构造函数接受一个带有两个参数的函数:resolvereject。这些函数用于将 Promise 从 pending 状态转换为 fulfilledrejected

如果在 executor 函数内部抛出错误,Promise 将被拒绝并显示该错误。 executor 函数的返回值将被忽略:只有 resolvereject 应用于确定 Promise。

const myPromise = new Promise((resolve, reject) => {
  let success = true;

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

在上面的例子中

  • 如果 success 条件为 true,则 Promise 被 fulfilled,并且值 'Operation was successful!' 传递给 resolve 函数。
  • 如果 success 条件为 false,则 Promise 被 rejected,并且错误 'Something went wrong.' 传递给 reject 函数。

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

创建 Promise 后,您可以使用 .then().catch().finally() 方法处理结果。

  • .then() 用于处理 fulfilled 的 Promise 并访问其结果。
  • .catch() 用于处理 rejected 的 Promise 并捕获可能发生的任何错误。
  • .finally() 用于处理 settled 的 Promise,无论 Promise 是 resolved 还是 rejected。
const myPromise = new Promise((resolve, reject) => {
  let success = true;

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

myPromise
  .then(result => {
    console.log(result); // This will run if the Promise is fulfilled
  })
  .catch(error => {
    console.error(error); // This will run if the Promise is rejected
  })
  .finally(() => {
    console.log('The promise has completed'); // This will run when the Promise is settled
  });

链式 Promise

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

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

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

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

将 Async/Await 与 Promise 一起使用

在现代 JavaScript 中使用 Promise 的最佳方法之一是使用 async/await。这允许您编写看起来同步的异步代码,使其更易于阅读和维护。

  • async 用于定义返回 Promise 的函数。
  • awaitasync 函数内部使用,用于暂停执行,直到 Promise 确定。
async function performTasks() {
  try {
    const result1 = await promise1;
    console.log(result1); // 'First task completed'

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

performTasks();

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

本质上,上面的代码的执行结果与用户编写以下代码相同

promise1
  .then(function (result1) {
    console.log(result1);
    return promise2;
  })
  .then(function (result2) {
    console.log(result2);
  })
  .catch(function (error) {
    console.log(error);
  });

顶层 Await

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

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

await delay(1000);

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

基于 Promise 的 Node.js API

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

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

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

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}

readFile();

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

高级 Promise 方法

JavaScript 的 Promise 全局变量提供了几种强大的方法,可以更有效地管理多个异步任务

Promise.all()

此方法接受一个 Promise 数组,并返回一个新的 Promise,该 Promise 在所有 Promise 都 fulfilled 后解析。 如果任何 Promise 被 rejected,Promise.all() 将立即 reject。 但是,即使发生 reject,Promise 也会继续执行。 当处理大量 Promise 时,尤其是在批量处理中,使用此函数可能会影响系统的内存。

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

const fetchData1 = delay(1000).then(() => 'Data from API 1');
const fetchData2 = delay(2000).then(() => 'Data from API 2');

Promise.all([fetchData1, fetchData2])
  .then(results => {
    console.log(results); // ['Data from API 1', 'Data from API 2']
  })
  .catch(error => {
    console.error('Error:', error);
  });

Promise.allSettled()

此方法等待所有 promise resolve 或 reject,并返回一个对象数组,描述每个 Promise 的结果。

const promise1 = Promise.resolve('Success');
const promise2 = Promise.reject('Failed');

Promise.allSettled([promise1, promise2]).then(results => {
  console.log(results);
  // [ { status: 'fulfilled', value: 'Success' }, { status: 'rejected', reason: 'Failed' } ]
});

Promise.all() 不同,Promise.allSettled() 不会在失败时短路。 它等待所有 promise 完成,即使某些 promise reject。 这为批量操作提供了更好的错误处理,您可能需要知道所有任务的状态,而不管是否失败。

Promise.race()

此方法在第一个 Promise 确定后立即 resolve 或 reject,无论它是 resolve 还是 reject。 无论哪个 promise 首先确定,所有 promise 都将完全执行。

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

const task1 = delay(2000).then(() => 'Task 1 done');
const task2 = delay(1000).then(() => 'Task 2 done');

Promise.race([task1, task2]).then(result => {
  console.log(result); // 'Task 2 done' (since task2 finishes first)
});

Promise.any()

此方法在其中一个 Promise resolve 后立即 resolve。 如果所有 promise 都被 reject,它将 reject 并显示 AggregateError

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

const api1 = delay(2000).then(() => 'API 1 success');
const api2 = delay(1000).then(() => 'API 2 success');
const api3 = delay(1500).then(() => 'API 3 success');

Promise.any([api1, api2, api3])
  .then(result => {
    console.log(result); // 'API 2 success' (since it resolves first)
  })
  .catch(error => {
    console.error('All promises rejected:', error);
  });

Promise.reject()Promise.resolve()

这些方法直接创建 rejected 或 resolved 的 Promise。

Promise.resolve('Resolved immediately').then(result => {
  console.log(result); // 'Resolved immediately'
});

Promise.try()

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

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

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

Promise.try(mightThrow)
  .then(result => {
    console.log('Result:', result);
  })
  .catch(err => {
    console.error('Caught error:', err.message);
  });

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

Promise.withResolvers()

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

const { promise, resolve, reject } = Promise.withResolvers();

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

promise.then(value => {
  console.log('Success:', value);
});

在此示例中,Promise.withResolvers() 使您可以完全控制何时以及如何 resolve 或 reject promise,而无需内联定义 executor 函数。 此模式通常用于事件驱动的编程、超时或与基于非 promise 的 API 集成时。

Promise 的错误处理

处理 Promise 中的错误可确保您的应用程序在发生意外情况时正常运行。

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

performTask();

在事件循环中调度任务

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

queueMicrotask()

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

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

console.log('Synchronous task is executed');

在上面的例子中,"Microtask is executed" 将在 "Synchronous task is executed" 之后,但在任何 I/O 操作(如定时器)之前被记录。

process.nextTick()

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

process.nextTick(() => {
  console.log('Next tick callback');
});

console.log('Synchronous task executed');

setImmediate()

setImmediate() 用于在当前事件循环周期结束后,并且所有 I/O 事件都已处理后,执行回调函数。 这意味着 setImmediate() 回调函数在任何 I/O 回调函数之后运行,但在定时器之前运行。

setImmediate(() => {
  console.log('Immediate callback');
});

console.log('Synchronous task executed');

何时使用它们

  • 对于需要在当前脚本之后,以及任何 I/O 或定时器回调函数之前立即运行的任务,通常用于 Promise 解析,请使用 queueMicrotask()
  • 对于应该在任何 I/O 事件之前执行的任务,通常用于延迟操作或同步处理错误,请使用 process.nextTick()
  • 对于应该在 I/O 事件之后但在定时器之前运行的任务,请使用 setImmediate()

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

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