JS `Promise.allSettled()` (ES2021):获取所有 Promise 的结果,不因失败中断

各位观众老爷们,大家好!今天咱们来聊聊 JavaScript 里一个非常有用的家伙——Promise.allSettled()。这玩意儿啊,就像一个经验丰富的老船长,能带着你所有的 Promise 兄弟们安全靠岸,不管他们是顺风顺水还是触礁搁浅,一个都不落下。

Promise.allSettled():啥是 settled

首先,咱们得搞明白啥叫 settled。在 Promise 的世界里,settled 可不是指“安顿好了”的意思,而是指 Promise 的状态已经确定了,要么是 fulfilled(成功兑现),要么是 rejected(失败拒绝)。总之,就是尘埃落定,有了最终结果。

Promise.allSettled() 的作用,就是接收一个 Promise 数组(或者任何可迭代的 Promise),等待所有 Promise 都变成 settled 状态,然后返回一个包含每个 Promise 结果的数组。重点来了:它不会因为其中某个 Promise 失败而中断! 这和 Promise.all() 可不一样,Promise.all() 只要有一个 Promise 失败,整个操作就直接完蛋大吉,剩下的 Promise 结果都不要了。

语法和返回值

Promise.allSettled() 的语法非常简单:

Promise.allSettled(iterable);

其中 iterable 是一个可迭代对象,比如数组,里面的每个元素都应该是 Promise。

返回值是一个 Promise,它会在所有输入的 Promise 都 settled 后 resolve。这个 resolve 的值是一个数组,数组里的每个元素都是一个对象,描述了对应 Promise 的结果。这个对象有两种形式:

  • 如果 Promise fulfilled (成功):

    { status: 'fulfilled', value: <Promise 的 resolve 值> }
  • 如果 Promise rejected (失败):

    { status: 'rejected', reason: <Promise 的 reject 原因> }

咱们来看个例子:

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises)
  .then((results) => results.forEach((result) => console.log(result)));

// 预期输出:
// { status: 'fulfilled', value: 3 }
// { status: 'rejected', reason: 'foo' }

在这个例子里,promise1 立即 resolve 成了 3,promise2 延迟 100 毫秒后 reject 了 ‘foo’。Promise.allSettled() 等待这两个 Promise 都 settled 后,返回了一个包含两个对象的数组。注意,即使 promise2 失败了,整个操作并没有中断,而是继续执行,并记录了 promise2 的失败原因。

为什么要用 Promise.allSettled()

你可能会问,既然 Promise.all() 更简单,为啥还要用 Promise.allSettled() 呢? 答案就是:容错性

想象一下,你正在做一个批量处理的任务,需要并行执行多个操作,每个操作都对应一个 Promise。如果其中一个操作失败了,你肯定不希望整个任务都崩溃,而是希望能够继续执行其他的操作,并记录下失败的操作,以便后续处理。

Promise.allSettled() 正好可以满足这种需求。它可以确保所有 Promise 都会执行完毕,无论成功还是失败,让你能够全面掌握任务的执行情况。

实际应用场景

Promise.allSettled() 在很多场景下都非常有用,比如:

  1. 批量上传文件: 你需要同时上传多个文件,但有些文件可能上传失败(比如网络问题、文件格式错误等)。使用 Promise.allSettled() 可以确保所有文件都尝试上传,并记录下上传失败的文件,方便用户重试。

  2. 并行请求 API: 你需要同时请求多个 API 接口,但有些接口可能返回错误。使用 Promise.allSettled() 可以确保所有接口都请求完毕,并记录下返回错误的接口,方便后续分析。

  3. 数据验证: 你需要同时验证多个数据项,但有些数据项可能验证失败。使用 Promise.allSettled() 可以确保所有数据项都得到验证,并记录下验证失败的数据项,方便用户修改。

代码示例

咱们来看几个更具体的代码示例,让你对 Promise.allSettled() 的用法有更直观的了解。

示例 1:批量上传文件

async function uploadFiles(files) {
  const uploadPromises = files.map(file => {
    return new Promise((resolve, reject) => {
      // 模拟上传文件
      setTimeout(() => {
        const random = Math.random();
        if (random > 0.5) {
          console.log(`文件 ${file.name} 上传成功`);
          resolve({ filename: file.name, url: 'http://example.com/' + file.name });
        } else {
          console.log(`文件 ${file.name} 上传失败`);
          reject(new Error(`文件 ${file.name} 上传失败`));
        }
      }, Math.random() * 1000); // 模拟上传时间
    });
  });

  const results = await Promise.allSettled(uploadPromises);

  const successfulUploads = results.filter(result => result.status === 'fulfilled')
    .map(result => result.value);

  const failedUploads = results.filter(result => result.status === 'rejected')
    .map(result => ({ filename: result.reason.message.replace('文件 ', '').replace(' 上传失败', ''), error: result.reason }));

  console.log('上传成功的文件:', successfulUploads);
  console.log('上传失败的文件:', failedUploads);

  return { successfulUploads, failedUploads };
}

// 模拟文件列表
const files = [
  { name: 'file1.txt', size: 1024 },
  { name: 'file2.jpg', size: 2048 },
  { name: 'file3.pdf', size: 4096 },
];

uploadFiles(files);

在这个例子里,uploadFiles 函数接收一个文件数组,并为每个文件创建一个上传 Promise。每个 Promise 模拟上传文件,并随机成功或失败。Promise.allSettled() 等待所有 Promise 都 settled 后,将结果分成上传成功和上传失败两部分,方便后续处理。

示例 2:并行请求 API

async function fetchData(urls) {
  const fetchPromises = urls.map(url => {
    return fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .catch(error => {
        console.error(`请求 ${url} 失败:`, error);
        throw error; // 重新抛出错误,让 Promise.allSettled 捕获
      });
  });

  const results = await Promise.allSettled(fetchPromises);

  const successfulResponses = results.filter(result => result.status === 'fulfilled')
    .map(result => result.value);

  const failedResponses = results.filter(result => result.status === 'rejected')
    .map(result => ({ url: result.reason.message.match(/https?://[^s]+/)[0], error: result.reason })); // 从错误信息中提取 URL

  console.log('成功响应:', successfulResponses);
  console.log('失败响应:', failedResponses);

  return { successfulResponses, failedResponses };
}

// 模拟 API 接口列表
const urls = [
  'https://jsonplaceholder.typicode.com/todos/1',
  'https://jsonplaceholder.typicode.com/posts/1',
  'https://jsonplaceholder.typicode.com/users/1',
  'https://httpstat.us/500', // 模拟一个返回 500 错误的接口
];

fetchData(urls);

在这个例子里,fetchData 函数接收一个 URL 数组,并为每个 URL 创建一个 fetch Promise。每个 Promise 请求对应的 API 接口,并处理可能发生的错误。Promise.allSettled() 等待所有 Promise 都 settled 后,将结果分成成功响应和失败响应两部分,方便后续分析。

示例 3:数据验证

async function validateData(data) {
  const validationPromises = Object.entries(data).map(([key, value]) => {
    return new Promise((resolve, reject) => {
      // 模拟数据验证
      setTimeout(() => {
        if (typeof value === 'string' && value.length > 5) {
          console.log(`数据项 ${key} 验证通过`);
          resolve({ key, valid: true });
        } else {
          console.log(`数据项 ${key} 验证失败`);
          reject(new Error(`数据项 ${key} 验证失败:长度必须大于 5`));
        }
      }, Math.random() * 500); // 模拟验证时间
    });
  });

  const results = await Promise.allSettled(validationPromises);

  const validData = results.filter(result => result.status === 'fulfilled')
    .map(result => result.value);

  const invalidData = results.filter(result => result.status === 'rejected')
    .map(result => ({ key: result.reason.message.replace('数据项 ', '').replace(' 验证失败:长度必须大于 5', ''), error: result.reason }));

  console.log('验证通过的数据:', validData);
  console.log('验证失败的数据:', invalidData);

  return { validData, invalidData };
}

// 模拟数据
const data = {
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  city: 'New York',
  country: 'US',
  shortString: 'abc',
};

validateData(data);

在这个例子里,validateData 函数接收一个数据对象,并为每个数据项创建一个验证 Promise。每个 Promise 模拟数据验证,并根据验证规则成功或失败。Promise.allSettled() 等待所有 Promise 都 settled 后,将结果分成验证通过和验证失败两部分,方便用户修改。

Promise.allSettled() vs Promise.all() vs Promise.race() vs Promise.any()

为了更好地理解 Promise.allSettled() 的特点,咱们把它和其他几个常用的 Promise 方法做一个对比:

方法 描述 容错性
Promise.all() 接收一个 Promise 数组,等待所有 Promise 都 fulfilled 后 resolve。只要有一个 Promise rejected,就立即 reject。
Promise.allSettled() 接收一个 Promise 数组,等待所有 Promise 都 settled 后 resolve。无论 Promise fulfilled 还是 rejected,都会记录结果,不会中断。
Promise.race() 接收一个 Promise 数组,只要有一个 Promise settled,就立即 resolve 或 reject,并返回该 Promise 的结果。
Promise.any() 接收一个 Promise 数组,只要有一个 Promise fulfilled,就立即 resolve,并返回该 Promise 的结果。如果所有 Promise 都 rejected,则 reject 并返回一个 AggregateError 错误,包含所有 Promise 的 reject 原因。(ES2021 新增)

从表格中可以看出,Promise.allSettled() 在容错性方面是最强的,它能够确保所有 Promise 都执行完毕,并记录下所有结果,而不会因为某个 Promise 的失败而中断。

总结

Promise.allSettled() 是一个非常有用的 Promise 方法,特别是在需要处理多个可能失败的 Promise 时。它可以确保所有 Promise 都执行完毕,并记录下所有结果,让你能够全面掌握任务的执行情况。记住,它就像一个经验丰富的老船长,能带着你所有的 Promise 兄弟们安全靠岸,一个都不落下!

好了,今天的讲座就到这里。希望大家能够掌握 Promise.allSettled() 的用法,并在实际开发中灵活运用。下次再见!

发表回复

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