探索 Node.js 中的 Promise
Promise 是 JavaScript 中的一个特殊对象,表示一个异步操作的最终完成(或失败)及其结果值。本质上,Promise 是一个尚未可用但将来会有的值的占位符。
把 Promise 想象成点披萨:你不会立刻拿到它,但外卖员承诺稍后会送到。你不知道*确切*的时间,但你知道结果要么是“披萨已送达”,要么是“出了点问题”。
Promise 的状态
一个 Promise 可以处于以下三种状态之一:
- 待定(Pending):初始状态,异步操作仍在进行中。
- 已兑现(Fulfilled):操作成功完成,Promise 现在已用一个值解决(resolved)。
- 已拒绝(Rejected):操作失败,Promise 因一个原因(通常是错误)而敲定(settled)。
当你点披萨时,你处于待定状态,饥饿而充满希望。如果披萨热腾腾、香喷喷地送达,你就进入了已兑现状态。但如果餐厅打电话说他们把你的披萨掉地上了,你就处于已拒绝状态。
无论你的晚餐是喜是悲,一旦有了最终结果,Promise 就被认为是已敲定(settled)。
Promise 的基本语法
创建 Promise 最常见的方法之一是使用 new Promise()
构造函数。该构造函数接受一个带有两个参数的函数:resolve
和 reject
。这两个函数用于将 Promise 从待定状态转换到已兑现或已拒绝状态。
如果在执行器函数内部抛出错误,Promise 将因该错误而被拒绝。执行器函数的返回值会被忽略:只应使用 resolve
或 reject
来敲定 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 事件循环。