Promise 与 async/await

在上一章中,我们学习了回调函数,它是 Node.js 异步编程的基础。然而,当异步操作嵌套过多时,回调会导致代码难以维护。ES6 引入的 Promise 提供了一种更优雅的异步编程模型,而 ES2017 的 async/await 则让异步代码几乎与同步代码一样简洁。本章将深入讲解 Promise 和 async/await 的使用方法。

1. 什么是 Promise?

Promise 是一个表示异步操作最终完成或失败的对象。它可以处于三种状态之一:

  • pending(进行中):初始状态,既未完成也未拒绝。
  • fulfilled(已成功):操作成功完成,此时会有一个结果值。
  • rejected(已失败):操作失败,此时会有一个错误原因。

Promise 的状态一旦改变,就不会再变(从 pending 变为 fulfilled 或 rejected)。

2. 创建 Promise

使用 new Promise 构造函数创建 Promise,它接受一个执行器函数,该函数有两个参数:resolvereject,分别用于将 Promise 标记为成功或失败。

const myPromise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功');
    } else {
      reject(new Error('操作失败'));
    }
  }, 1000);
});

3. 使用 Promise:then、catch、finally

Promise 实例具有 thencatchfinally 方法,用于处理结果或错误。

myPromise
  .then(result => {
    console.log('成功:', result);
  })
  .catch(error => {
    console.error('失败:', error);
  })
  .finally(() => {
    console.log('无论成功或失败都会执行');
  });

thencatch 都返回一个新的 Promise,因此可以链式调用。

4. 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

5. 错误处理

除了在链尾使用 catch,也可以在 then 中提供第二个回调函数处理错误(但通常不推荐,因为会捕获不到之前链的错误)。

// 推荐:统一在链尾 catch
asyncTask(1)
  .then(result => {
    throw new Error('模拟错误');
  })
  .then(result => {
    console.log('不会执行');
  })
  .catch(error => {
    console.log('捕获到错误:', error.message);
  });

6. Promise 的静态方法

Promise 类提供了几个有用的静态方法。

6.1 Promise.all(iterable)

接收一个 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"]
});

6.2 Promise.race(iterable)

返回一个新的 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); // '快'
});

6.3 Promise.allSettled(iterable)

等待所有 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: '错误' }
  // ]
});

6.4 Promise.any(iterable)

只要有一个 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); // '成功'
});

7. 将回调函数转换为 Promise

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

8. async/await

async/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码,更加简洁易读。

8.1 async 函数

在函数声明前加上 async 关键字,该函数会返回一个 Promise。即使函数内部返回一个非 Promise 值,也会被自动包装成 resolved 的 Promise。

async function greet() {
  return 'Hello';
}

greet().then(console.log); // 'Hello'

8.2 await 表达式

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();

8.3 并发执行

如果多个异步操作没有依赖关系,可以使用 Promise.all 并发执行,而不是顺序 await

// 错误:顺序执行,耗时更长
const user = await getUser(id);
const posts = await getPosts(id);

// 正确:并发执行
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);

8.4 循环中的 async/await

在循环中如果希望顺序执行异步操作,可以使用 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)));
}

9. 错误处理最佳实践

  • 始终使用 try/catch 包裹 await 调用,或者在 Promise 链最后添加 catch
  • 避免在 async 函数中抛出未捕获的错误,确保错误被处理。
  • 使用 Promise.all 时注意:任何一个 Promise 失败都会导致整体失败。如果需要容忍个别失败,可以使用 allSettled

10. 综合示例:读取多个文件并合并

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));

11. 异步迭代器与 for-await-of

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
  }
})();

12. 常见陷阱

  • 忘记 await:在 async 函数中忘记 await 会得到未完成的 Promise,可能导致后续代码错误。
  • 不恰当的并发:在不必要时使用 Promise.all 而不是顺序 await 可能导致资源竞争或意外副作用。
  • 忽略错误处理:未捕获的 Promise 拒绝会导致 UnhandledPromiseRejectionWarning。
总结:
  • Promise 提供了一种标准化的异步结果处理方式。
  • 通过链式调用和静态方法,可以轻松组合多个异步操作。
  • async/await 让异步代码更简洁,但需要理解其背后的 Promise。
  • 现代 Node.js 开发中,建议优先使用 Promise 和 async/await 代替原始回调。

小结

本章详细介绍了 Promise 的用法、静态方法和 async/await 的语法。掌握这些知识后,你将能编写出更加清晰、可维护的异步代码。在接下来的章节中,我们将探讨 Node.js 中的事件循环机制,深入理解异步背后的原理。