JS `Promise.allSettled` (ES2021):等待所有 Promise 完成,无论成功或失败

各位观众,晚上好!今儿咱们聊聊 JavaScript 里头一个挺实用,但有时候又容易被忽略的家伙:Promise.allSettled。这玩意儿啊,能让你在处理一堆 Promise 的时候,甭管它们是成功还是失败,都能安安心心地把结果都拿到手。不像 Promise.all 那样,只要有一个 Promise 崩了,整个就歇菜了。

啥是 Promise.allSettled

简单来说,Promise.allSettled 接收一个 Promise 数组(或者任何可迭代的 Promise ),然后它会等待数组里的所有 Promise 都完成(resolved 或 rejected)。 无论每个 Promise 的结果如何,Promise.allSettled 都会返回一个包含所有 Promise 结果的数组。

这个结果数组的每个元素都是一个对象,包含两个属性:

  • status: 字符串,表示 Promise 的状态,可能是 "fulfilled" (成功) 或 "rejected" (失败)。
  • value: 如果 status"fulfilled",则包含 Promise 的 resolved 值。
  • reason: 如果 status"rejected",则包含 Promise 的 rejected 原因。

为什么需要 Promise.allSettled

想象一下,你在做一个批量处理的任务,需要调用多个 API 接口。如果其中一个接口挂了,你肯定不想整个任务都失败吧?Promise.allSettled 就能帮你搞定这个场景。它可以让你知道哪些接口成功了,哪些接口失败了,然后你可以针对失败的接口进行重试或者其他处理。

再比如,你要同时加载多个资源,但有些资源可能加载失败。使用 Promise.allSettled,你可以加载所有能加载的资源,然后忽略加载失败的资源,而不是直接抛出错误导致整个页面崩溃。

Promise.allSettled 的基本用法

咱们先来看一个简单的例子:

const promise1 = Promise.resolve(123);
const promise2 = Promise.reject("出错了!");
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 2000, "成功了!"));

Promise.allSettled([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
  });

这段代码的输出大概是这样的(注意,promise3 的完成时间会影响最终输出的顺序):

[
  { "status": "fulfilled", "value": 123 },
  { "status": "rejected", "reason": "出错了!" },
  { "status": "fulfilled", "value": "成功了!" }
]

可以看到,Promise.allSettled 返回的数组包含了每个 Promise 的状态和结果。 即使 promise2 失败了,整个 Promise.allSettled 仍然成功完成,并且返回了 promise2 的 rejected 原因。

Promise.allSettledPromise.all 的区别

特性 Promise.all Promise.allSettled
失败处理 只要有一个 Promise 失败,整个 Promise 就立即失败。 等待所有 Promise 完成,即使有 Promise 失败。
返回值 返回一个 Promise,resolved 值为所有 Promise 的结果数组。 返回一个 Promise,resolved 值为包含每个 Promise 状态和结果的数组。
适用场景 所有 Promise 都必须成功才能继续的场景。 需要处理部分 Promise 失败的情况,并且仍然需要知道所有 Promise 的结果的场景。

简单来说,Promise.all 就像一个团队,只要有一个人掉链子,整个团队就完蛋。而 Promise.allSettled 就像一个探险队,即使有人受伤了,探险队仍然会完成任务,并且记录下每个人的情况。

实际应用场景

1. 并行请求多个 API

async function fetchData(urls) {
  const promises = urls.map(url =>
    fetch(url)
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
      .catch(error => {
        console.error(`Error fetching ${url}: ${error}`);
        return null; // 返回 null 或其他默认值,避免 Promise.all 提前终止
      })
  );

  const results = await Promise.allSettled(promises);

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

  const failedUrls = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason);

  console.log("成功获取的数据:", successfulData);
  console.log("失败的 URLs:", failedUrls);

  return { successfulData, failedUrls };
}

const urls = [
  "https://jsonplaceholder.typicode.com/todos/1",
  "https://jsonplaceholder.typicode.com/posts/1",
  "https://jsonplaceholder.typicode.com/invalid-url"
];

fetchData(urls);

在这个例子中,我们并行请求了三个 API 接口。即使其中一个接口 (https://jsonplaceholder.typicode.com/invalid-url) 返回了错误,Promise.allSettled 仍然会等待其他接口完成,并且返回所有接口的结果。 这样我们就可以知道哪些接口成功了,哪些接口失败了,然后进行相应的处理。

2. 加载多个资源

async function loadImages(imageUrls) {
  const promises = imageUrls.map(url =>
    new Promise(resolve => {
      const img = new Image();
      img.onload = () => {
        console.log(`Image loaded: ${url}`);
        resolve({ url: url, element: img });
      };
      img.onerror = () => {
        console.error(`Failed to load image: ${url}`);
        resolve({ url: url, error: new Error(`Failed to load image: ${url}`) }); // 这里resolve,而不是reject
      };
      img.src = url;
    })
  );

  const results = await Promise.allSettled(promises);

  const loadedImages = results
    .filter(result => result.status === 'fulfilled' && !result.value.error)
    .map(result => result.value.element);

  const failedImageUrls = results
    .filter(result => result.status === 'fulfilled' && result.value.error)
    .map(result => result.value.url);

  console.log("成功加载的图片:", loadedImages);
  console.log("加载失败的图片 URLs:", failedImageUrls);

  return { loadedImages, failedImageUrls };
}

const imageUrls = [
  "https://via.placeholder.com/150",
  "https://via.placeholder.com/200",
  "https://invalid-image-url.com/image.jpg"
];

loadImages(imageUrls);

在这个例子中,我们尝试加载多个图片。即使其中一个图片加载失败,Promise.allSettled 仍然会等待其他图片加载完成,并且返回所有图片的结果。 这样我们就可以显示所有成功加载的图片,并且忽略加载失败的图片,而不是直接抛出错误导致整个页面崩溃。注意这里onerror中要resolve而不是reject,因为Promise.allSettled 需要所有promise都settled,所以要通过resolve来告知promise已经完成。

3. 表单验证

async function validateForm(formData) {
  const validationPromises = [
    validateUsername(formData.username),
    validateEmail(formData.email),
    validatePassword(formData.password)
  ];

  const results = await Promise.allSettled(validationPromises);

  const errors = results
    .filter(result => result.status === 'rejected')
    .map(result => result.reason);

  if (errors.length > 0) {
    console.error("表单验证失败:", errors);
    return { isValid: false, errors: errors };
  } else {
    console.log("表单验证成功!");
    return { isValid: true };
  }
}

async function validateUsername(username) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (username.length < 5) {
        reject("用户名长度必须大于等于 5 个字符");
      } else {
        resolve();
      }
    }, 500);
  });
}

async function validateEmail(email) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (!email.includes('@')) {
        reject("邮箱格式不正确");
      } else {
        resolve();
      }
    }, 300);
  });
}

async function validatePassword(password) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (password.length < 8) {
        reject("密码长度必须大于等于 8 个字符");
      } else {
        resolve();
      }
    }, 700);
  });
}

const formData = {
  username: "user",
  email: "invalid-email",
  password: "short"
};

validateForm(formData);

在这个例子中,我们并行验证表单中的用户名、邮箱和密码。即使其中一个验证失败,Promise.allSettled 仍然会等待其他验证完成,并且返回所有验证的结果。 这样我们就可以知道哪些字段验证失败了,并且将错误信息显示给用户。

Promise.allSettled 的一些注意事项

  • 错误处理: Promise.allSettled 不会抛出任何错误。 你需要自己检查返回结果数组中的每个 Promise 的状态,并且根据状态进行相应的处理。
  • 顺序: Promise.allSettled 返回的结果数组的顺序与传入的 Promise 数组的顺序相同。
  • 性能: Promise.allSettled 会等待所有 Promise 都完成,所以如果其中一个 Promise 花费很长时间才能完成,那么整个 Promise.allSettled 也会花费很长时间。

Promise.allSettledPromise.any 的对比 (ES2021)

Promise.anyPromise.allSettled 都是 ES2021 引入的,它们都用于处理多个 Promise。 但它们的行为完全不同。

  • Promise.any: 只要有一个 Promise 成功,就立即返回该 Promise 的 resolved 值。 如果所有 Promise 都失败了,则抛出一个 AggregateError 错误。
  • Promise.allSettled: 等待所有 Promise 完成,无论成功或失败,然后返回一个包含每个 Promise 状态和结果的数组。
特性 Promise.any Promise.allSettled
成功条件 只要有一个 Promise 成功,就立即成功。 必须等待所有 Promise 完成。
失败条件 所有 Promise 都失败,才失败。 永远不会失败,总是返回一个 resolved 的 Promise。
返回值 返回第一个成功的 Promise 的 resolved 值。 返回一个包含每个 Promise 状态和结果的数组。
错误处理 如果所有 Promise 都失败,则抛出一个 AggregateError 错误。 需要自己检查返回结果数组中的每个 Promise 的状态,并且根据状态进行相应的处理。
适用场景 只需要一个 Promise 成功即可的场景,例如:尝试多个服务器,只要有一个服务器可用即可。 需要知道所有 Promise 的结果的场景,无论成功或失败。

手动实现 Promise.allSettled (Polyfill)

如果你的运行环境不支持 Promise.allSettled,你可以自己实现一个 polyfill:

if (!Promise.allSettled) {
  Promise.allSettled = function(promises) {
    return Promise.all(
      Array.from(promises).map(promise => {
        return Promise.resolve(promise)
          .then(
            value => ({ status: 'fulfilled', value: value }),
            reason => ({ status: 'rejected', reason: reason })
          );
      })
    );
  };
}

这个 polyfill 的原理很简单:

  1. 将传入的 Promise 数组转换为一个数组。
  2. 使用 map 方法遍历数组中的每个 Promise。
  3. 对于每个 Promise,使用 Promise.resolve 将其转换为一个 Promise 对象(如果它还不是)。
  4. 使用 then 方法处理 Promise 的成功和失败情况。
  5. 如果 Promise 成功,则返回一个包含 status"fulfilled"value 为 Promise 的 resolved 值的对象。
  6. 如果 Promise 失败,则返回一个包含 status"rejected"reason 为 Promise 的 rejected 原因的对象。
  7. 使用 Promise.all 等待所有 Promise 完成,并且返回包含每个 Promise 状态和结果的数组。

总结

Promise.allSettled 是一个非常有用的工具,可以让你在处理多个 Promise 时更加灵活和可靠。 它可以让你知道哪些 Promise 成功了,哪些 Promise 失败了,然后你可以针对失败的 Promise 进行重试或者其他处理。 记住,它和 Promise.all 的行为完全不同,不要混淆使用。

希望今天的讲座对大家有所帮助! 咱们下次再见!

发表回复

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