JS `Promise.any` 与 `AggregateError` (ES2021):获取第一个成功的 Promise

好了,各位同学,今天咱们来聊聊 JavaScript 里一个挺有意思的家伙 —— Promise.any。这家伙专门负责在“千军万马过独木桥”的情况下,帮你抢到第一个成功的 Promise。当然,如果大家都掉进坑里了,它还会给你留下一个“集体阵亡通知书”—— AggregateError

咱们先来认识一下 Promise.any

一、Promise.any:只要有一个行,就万事大吉

Promise.any 就像一个乐观主义者,它会接收一个 Promise 数组(或者任何 iterable 对象,只要里面的元素能被解析成 Promise 就行)。它会并行地执行这些 Promise,然后……

  • 如果其中一个 Promise 成功了Promise.any 会立即 resolve,带着这个成功的 Promise 的结果。记住,它只关心第一个成功的,其他的它才懒得管呢。
  • 如果所有的 Promise 都失败了Promise.any 会 reject,带着一个 AggregateError。这个错误里面包含了所有失败的 Promise 的原因。

听起来是不是有点像“多人赛跑,谁先到终点算谁赢”?

1.1 基础用法:简单示例

咱们先来个简单的例子热热身:

const promise1 = Promise.reject('Promise 1 失败了');
const promise2 = Promise.resolve('Promise 2 成功了');
const promise3 = Promise.reject('Promise 3 也失败了');

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log('成功了:', value); // 输出: 成功了: Promise 2 成功了
  })
  .catch((error) => {
    console.log('全都失败了:', error);
  });

在这个例子里,promise2 第一个成功,所以 Promise.any 就直接 resolve 并且带着 promise2 的结果了。promise1promise3 的失败,它根本没放在眼里。

1.2 所有 Promise 都失败的情况:AggregateError 登场

如果所有的 Promise 都失败了,Promise.any 会抛出一个 AggregateError。这个错误对象有一个 errors 属性,它是一个数组,包含了所有失败的 Promise 的原因。

const promise1 = Promise.reject('Promise 1 失败了');
const promise2 = Promise.reject('Promise 2 也失败了');
const promise3 = Promise.reject('Promise 3 还是失败了');

Promise.any([promise1, promise2, promise3])
  .then((value) => {
    console.log('成功了:', value); // 不会执行到这里
  })
  .catch((error) => {
    console.log('全都失败了:', error);
    console.log('失败原因:', error.errors);
  });

// 输出:
// 全都失败了: AggregateError: All promises were rejected
// 失败原因: [ 'Promise 1 失败了', 'Promise 2 也失败了', 'Promise 3 还是失败了' ]

可以看到,AggregateError 告诉我们所有 Promise 都失败了,并且通过 error.errors 属性,我们可以获取到每个 Promise 的失败原因。

1.3 与 Promise.race 的区别:一个是“胜者为王”,一个是“不成功,便成仁”

Promise.anyPromise.race 都是用来处理 Promise 数组的,但它们的行为却截然不同。

特性 Promise.any Promise.race
成功条件 只要有一个 Promise 成功,就 resolve 只要有一个 Promise resolve 或 reject,就 resolve/reject
失败条件 所有 Promise 都失败,才 reject,抛出 AggregateError 只要有一个 Promise reject,就 reject
适用场景 多个来源获取数据,只要一个成功即可 竞速,只需要最快的那个结果

简单来说:

  • Promise.race:谁跑得快,用谁的。不管是成功还是失败。
  • Promise.any:只要有人跑到终点(成功),就万事大吉。如果所有人都没跑到终点(都失败),那就集体失败。

二、AggregateError:失败者的墓志铭

AggregateError 是一个 Error 对象的子类,它专门用来表示多个操作同时失败的情况。它最常见的用途就是和 Promise.any 配合使用,当然,你也可以在其他需要表示多个错误的情况下使用它。

2.1 AggregateError 的属性

AggregateError 主要有一个属性:

  • errors:一个数组,包含了所有导致失败的原因。这些原因可以是 Error 对象,也可以是其他任何值。

2.2 自定义 AggregateError 的使用场景

虽然 AggregateError 经常和 Promise.any 一起出现,但你完全可以在自己的代码里使用它,来表示多个操作失败的情况。

例如,你有一个函数,需要并行地处理多个文件,如果其中一些文件处理失败了,你可以使用 AggregateError 来收集所有的错误信息:

async function processFiles(filePaths) {
  const promises = filePaths.map(async (filePath) => {
    try {
      // 模拟文件处理操作
      if (Math.random() < 0.5) {
        throw new Error(`文件 ${filePath} 处理失败`);
      }
      return `文件 ${filePath} 处理成功`;
    } catch (error) {
      return error; // 返回错误对象,而不是直接抛出
    }
  });

  const results = await Promise.all(promises);

  const errors = results.filter((result) => result instanceof Error);

  if (errors.length > 0) {
    throw new AggregateError(errors, '部分文件处理失败');
  }

  return results;
}

const filePaths = ['file1.txt', 'file2.txt', 'file3.txt'];

processFiles(filePaths)
  .then((results) => {
    console.log('所有文件处理成功:', results);
  })
  .catch((error) => {
    console.log('部分文件处理失败:', error);
    console.log('失败原因:', error.errors);
  });

在这个例子里,processFiles 函数会并行地处理多个文件。如果其中一些文件处理失败了,它会把这些错误收集到一个 errors 数组里,然后抛出一个 AggregateError,包含了所有的错误信息。

三、Promise.any 的应用场景:哪里需要,哪里搬

Promise.any 在很多场景下都非常有用,特别是在需要从多个来源获取数据,但只要一个来源成功就可以的情况下。

3.1 从多个 CDN 获取资源

假设你需要从多个 CDN 加载一个 JavaScript 库,但你不知道哪个 CDN 的速度最快或者最稳定。你可以使用 Promise.any 来尝试从多个 CDN 加载,只要有一个 CDN 成功加载了,就万事大吉:

const cdnUrls = [
  'https://cdn1.example.com/library.js',
  'https://cdn2.example.com/library.js',
  'https://cdn3.example.com/library.js',
];

const loadScript = (url) => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error(`加载 ${url} 失败`));
    document.head.appendChild(script);
  });
};

Promise.any(cdnUrls.map(loadScript))
  .then(() => {
    console.log('成功加载了 JavaScript 库');
  })
  .catch((error) => {
    console.log('所有 CDN 都加载失败了:', error);
  });

在这个例子里,Promise.any 会尝试从多个 CDN 加载 library.js。只要有一个 CDN 成功加载了,Promise.any 就会 resolve,并且执行 .then 里的代码。如果所有的 CDN 都加载失败了,Promise.any 就会 reject,并且执行 .catch 里的代码。

3.2 从多个 API 获取数据

假设你需要从多个 API 获取数据,但你不知道哪个 API 的响应最快或者最可靠。你可以使用 Promise.any 来尝试从多个 API 获取数据,只要有一个 API 成功返回了数据,就万事大吉:

const apiUrls = [
  'https://api1.example.com/data',
  'https://api2.example.com/data',
  'https://api3.example.com/data',
];

const fetchData = (url) => {
  return fetch(url)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`请求 ${url} 失败: ${response.status}`);
      }
      return response.json();
    });
};

Promise.any(apiUrls.map(fetchData))
  .then((data) => {
    console.log('成功获取到数据:', data);
  })
  .catch((error) => {
    console.log('所有 API 都请求失败了:', error);
  });

在这个例子里,Promise.any 会尝试从多个 API 获取数据。只要有一个 API 成功返回了数据,Promise.any 就会 resolve,并且执行 .then 里的代码。如果所有的 API 都请求失败了,Promise.any 就会 reject,并且执行 .catch 里的代码。

3.3 超时处理与备用方案

Promise.any 还可以结合 Promise.racesetTimeout 来实现超时处理和备用方案。例如,你可以设置一个超时时间,如果在超时时间内没有 API 返回数据,就使用一个备用数据:

const apiUrl = 'https://api.example.com/data';
const timeout = 5000; // 5 秒超时

const fetchDataWithTimeout = (url, timeout) => {
  return Promise.race([
    fetch(url)
      .then((response) => {
        if (!response.ok) {
          throw new Error(`请求 ${url} 失败: ${response.status}`);
        }
        return response.json();
      }),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    ),
  ]);
};

Promise.any([
  fetchDataWithTimeout(apiUrl, timeout),
  Promise.resolve({ message: '使用备用数据' }), // 备用数据
])
  .then((data) => {
    console.log('成功获取到数据:', data);
  })
  .catch((error) => {
    console.log('获取数据失败:', error);
  });

在这个例子里,fetchDataWithTimeout 函数使用 Promise.race 来实现超时处理。如果在超时时间内 API 没有返回数据,Promise.race 就会 reject,抛出一个 请求超时 的错误。然后,Promise.any 会尝试从 API 获取数据,如果 API 请求超时了,它会使用备用数据。

四、Promise.any 的兼容性:ES2021 新特性

Promise.any 是 ES2021 的新特性,因此在一些老旧的浏览器或者 Node.js 环境中可能不支持。在使用 Promise.any 之前,最好先检查一下运行环境是否支持。

  • 浏览器:现代浏览器(Chrome, Firefox, Safari, Edge)都支持 Promise.any
  • Node.js:Node.js v15 及以上版本支持 Promise.any

如果你的运行环境不支持 Promise.any,你可以使用 polyfill 来提供兼容性。例如,你可以使用 core-js 这个库来 polyfill Promise.any

npm install core-js

然后在你的代码里引入 core-js

require('core-js/features/promise/any');

// 现在你就可以使用 Promise.any 了

五、总结:Promise.any,你的备胎管理大师

Promise.any 是一个非常有用的工具,特别是在需要从多个来源获取数据,但只要一个来源成功就可以的情况下。它可以帮助你提高应用的可靠性和性能,并且可以简化你的代码。

记住,Promise.any 就像一个备胎管理大师,它会帮你找到最合适的“备胎”,并且在你需要的时候提供帮助。但是,如果所有的“备胎”都不靠谱,它也会给你留下一个“集体阵亡通知书”—— AggregateError,让你知道所有的问题所在。

总而言之,Promise.anyAggregateError 是 JavaScript 里一对好搭档,它们可以帮助你更好地处理异步操作,并且让你的代码更加健壮和可靠。

今天的讲座就到这里,希望大家对 Promise.anyAggregateError 有了更深入的了解。下次再遇到需要从多个来源获取数据的情况,不妨试试它们,相信它们会给你带来惊喜的。 下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注