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