在上一章中,我们学习了回调函数,它是 Node.js 异步编程的基础。然而,当异步操作嵌套过多时,回调会导致代码难以维护。ES6 引入的 Promise 提供了一种更优雅的异步编程模型,而 ES2017 的 async/await 则让异步代码几乎与同步代码一样简洁。本章将深入讲解 Promise 和 async/await 的使用方法。
Promise 是一个表示异步操作最终完成或失败的对象。它可以处于三种状态之一:
Promise 的状态一旦改变,就不会再变(从 pending 变为 fulfilled 或 rejected)。
使用 new Promise 构造函数创建 Promise,它接受一个执行器函数,该函数有两个参数:resolve 和 reject,分别用于将 Promise 标记为成功或失败。
const myPromise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功');
} else {
reject(new Error('操作失败'));
}
}, 1000);
});
Promise 实例具有 then、catch 和 finally 方法,用于处理结果或错误。
myPromise
.then(result => {
console.log('成功:', result);
})
.catch(error => {
console.error('失败:', error);
})
.finally(() => {
console.log('无论成功或失败都会执行');
});
then 和 catch 都返回一个新的 Promise,因此可以链式调用。
通过 then 的链式调用,可以按顺序执行多个异步操作,每个操作依赖上一个操作的结果。
function asyncTask(value) {
return new Promise(resolve => {
setTimeout(() => resolve(value * 2), 500);
});
}
asyncTask(1)
.then(result => {
console.log('第一步:', result); // 2
return asyncTask(result);
})
.then(result => {
console.log('第二步:', result); // 4
return asyncTask(result);
})
.then(result => {
console.log('第三步:', result); // 8
})
.catch(error => {
console.error('出错了:', error);
});
在链中,任何一个 Promise 被拒绝(rejected),都会跳过后续的 then 直到遇到 catch。
除了在链尾使用 catch,也可以在 then 中提供第二个回调函数处理错误(但通常不推荐,因为会捕获不到之前链的错误)。
// 推荐:统一在链尾 catch
asyncTask(1)
.then(result => {
throw new Error('模拟错误');
})
.then(result => {
console.log('不会执行');
})
.catch(error => {
console.log('捕获到错误:', error.message);
});
Promise 类提供了几个有用的静态方法。
接收一个 Promise 数组,返回一个新的 Promise。只有当所有 Promise 都成功时,才成功,返回所有结果的数组;只要有一个失败,就立即失败,返回第一个失败的原因。
const p1 = Promise.resolve(3);
const p2 = 42;
const p3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'));
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 42, "foo"]
});
返回一个新的 Promise,它随着第一个确定(成功或失败)的 Promise 而确定。
const p1 = new Promise(resolve => setTimeout(resolve, 500, '慢'));
const p2 = new Promise(resolve => setTimeout(resolve, 100, '快'));
Promise.race([p1, p2]).then(value => {
console.log(value); // '快'
});
等待所有 Promise 完成(无论成功或失败),返回一个对象数组,每个对象描述对应 Promise 的结果。
const p1 = Promise.resolve(3);
const p2 = Promise.reject('错误');
Promise.allSettled([p1, p2]).then(results => {
console.log(results);
// [
// { status: 'fulfilled', value: 3 },
// { status: 'rejected', reason: '错误' }
// ]
});
只要有一个 Promise 成功,就返回那个成功的值;如果所有 Promise 都失败,则返回一个 AggregateError 实例(包含所有失败原因)。
const p1 = Promise.reject('错误1');
const p2 = Promise.reject('错误2');
const p3 = Promise.resolve('成功');
Promise.any([p1, p2, p3]).then(value => {
console.log(value); // '成功'
});
Node.js 提供了 util.promisify 工具,可以将遵循错误优先回调的函数转换为返回 Promise 的函数。
const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
对于 Node.js 核心模块的许多方法,现在也提供了基于 Promise 的版本,例如 fs.promises。
async/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码,更加简洁易读。
在函数声明前加上 async 关键字,该函数会返回一个 Promise。即使函数内部返回一个非 Promise 值,也会被自动包装成 resolved 的 Promise。
async function greet() {
return 'Hello';
}
greet().then(console.log); // 'Hello'
await 只能在 async 函数内部使用,它会暂停函数的执行,等待一个 Promise 完成,并返回其结果。如果 Promise 被拒绝,则会抛出异常,可以用 try/catch 捕获。
async function readFileAsync() {
try {
const data = await fs.promises.readFile('file.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('读取失败:', err);
}
}
readFileAsync();
如果多个异步操作没有依赖关系,可以使用 Promise.all 并发执行,而不是顺序 await。
// 错误:顺序执行,耗时更长
const user = await getUser(id);
const posts = await getPosts(id);
// 正确:并发执行
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
在循环中如果希望顺序执行异步操作,可以使用 for...of 配合 await。如果希望并发执行,则使用 Promise.all。
// 顺序执行
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
}
// 并发执行
async function processItemsConcurrently(items) {
await Promise.all(items.map(item => processItem(item)));
}
try/catch 包裹 await 调用,或者在 Promise 链最后添加 catch。async 函数中抛出未捕获的错误,确保错误被处理。Promise.all 时注意:任何一个 Promise 失败都会导致整体失败。如果需要容忍个别失败,可以使用 allSettled。const fs = require('fs').promises;
async function mergeFiles(fileList) {
try {
const contents = await Promise.all(
fileList.map(file => fs.readFile(file, 'utf8'))
);
return contents.join('\n');
} catch (err) {
console.error('合并文件时出错:', err);
throw err; // 或返回默认值
}
}
mergeFiles(['a.txt', 'b.txt', 'c.txt'])
.then(result => console.log(result))
.catch(err => console.error('最终错误:', err));
ES2018 引入了异步迭代器和 for-await-of 循环,用于处理异步数据流,例如逐行读取文件或分页 API。
async function* asyncGenerator() {
for (let i = 0; i < 3; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
(async () => {
for await (const num of asyncGenerator()) {
console.log(num); // 0, 1, 2
}
})();
await 会得到未完成的 Promise,可能导致后续代码错误。Promise.all 而不是顺序 await 可能导致资源竞争或意外副作用。本章详细介绍了 Promise 的用法、静态方法和 async/await 的语法。掌握这些知识后,你将能编写出更加清晰、可维护的异步代码。在接下来的章节中,我们将探讨 Node.js 中的事件循环机制,深入理解异步背后的原理。