Promise.all(), Promise.race(), Promise.allSettled(), Promise.any() 的作用和区别是什么?请给出应用场景。

各位朋友,大家好! 今天咱们来聊聊JavaScript里Promise家族的几个扛把子:Promise.all(), Promise.race(), Promise.allSettled(), 和 Promise.any()。 别看名字有点绕,其实它们的功能和应用场景都挺有意思的。 咱们争取用大白话把它们讲清楚,再配上几个小例子,保证你听完就能上手。

开场白:Promise的那些事儿

在正式开始之前,咱们先简单回顾一下Promise。 Promise这玩意儿,说白了,就是用来处理异步操作的。 想象一下,你要去餐厅点菜,服务员告诉你菜可能要等一会儿,你不可能傻乎乎地一直站在那儿等吧? 你可以先干点别的,等菜做好了服务员再通知你。 Promise就扮演了这个“服务员”的角色,它代表着一个异步操作的最终结果,可能是成功,也可能是失败。

Promise有三种状态:

  • pending (等待中): 初始状态,表示异步操作尚未完成。
  • fulfilled (已成功): 异步操作成功完成。
  • rejected (已失败): 异步操作失败。

好了,有了这个基础,咱们就可以开始深入了解那四个Promise方法了。

第一位重量级选手:Promise.all()

Promise.all(),顾名思义,就是“全部都要”。 它接收一个Promise数组(或者任何可迭代对象,只要里面的元素都是Promise),只有当数组中所有的Promise都成功时,它才会成功,并返回一个包含所有Promise结果的数组(顺序和输入顺序一致)。 如果其中任何一个Promise失败Promise.all()就会立即失败,并返回第一个失败的Promise的reason。

应用场景:

  • 并发请求: 假设你需要同时从多个API获取数据,只有当所有数据都获取成功后,才能进行下一步操作。 比如,你需要获取用户的信息、用户的订单列表、用户的积分信息,这三个请求可以并行发送,只有全部成功才能渲染页面。
function getUserInfo(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve({ id: userId, name: '张三' });
      } else {
        reject('无效的用户ID');
      }
    }, 500); // 模拟网络延迟
  });
}

function getUserOrders(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (userId > 0) {
        resolve([{ orderId: 1, amount: 100 }, { orderId: 2, amount: 200 }]);
      } else {
        reject('无效的用户ID');
      }
    }, 300); // 模拟网络延迟
  });
}

function getUserPoints(userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (userId > 0) {
          resolve(500);
        } else {
          reject('无效的用户ID');
        }
      }, 400); // 模拟网络延迟
    });
  }

Promise.all([getUserInfo(1), getUserOrders(1), getUserPoints(1)])
  .then(results => {
    const [userInfo, userOrders, userPoints] = results;
    console.log('用户信息:', userInfo);
    console.log('用户订单:', userOrders);
    console.log('用户积分:', userPoints);
  })
  .catch(error => {
    console.error('获取数据失败:', error);
  });

// 如果userId为0,则会触发catch块
Promise.all([getUserInfo(0), getUserOrders(0)])
  .then(results => {
    // 不会执行到这里
  })
  .catch(error => {
    console.error('获取数据失败:', error); // 输出: 获取数据失败: 无效的用户ID
  });
  • 表单验证: 你可以同时验证多个表单字段,只有所有字段都验证通过,才能提交表单。

第二位急性子选手:Promise.race()

Promise.race(),顾名思义,就是“赛跑”。 它也接收一个Promise数组,但它只关心第一个完成的Promise,不管是成功还是失败。 哪个Promise最先完成,Promise.race()就返回那个Promise的结果(如果成功就返回resolve的值,如果失败就返回reject的原因)。

应用场景:

  • 超时控制: 你可以给一个请求设置超时时间,如果请求在规定时间内没有返回,就认为请求失败。
function fetchData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`从 ${url} 获取的数据`);
    }, 500); // 模拟网络延迟
  });
}

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求超时');
    }, ms);
  });
}

Promise.race([fetchData('https://example.com/api'), timeout(300)])
  .then(result => {
    console.log('请求成功:', result);
  })
  .catch(error => {
    console.error('请求失败:', error); // 输出: 请求失败: 请求超时
  });

// 如果将timeout时间设置为1000ms,则会输出:请求成功: 从 https://example.com/api 获取的数据
  • 备用方案: 你可以设置多个备用API,哪个API最先返回数据,就使用哪个API的结果。

第三位稳重选手:Promise.allSettled()

Promise.allSettled(),可以说是Promise.all()的升级版。 它也接收一个Promise数组,但它会等待所有Promise都完成,不管是成功还是失败。 然后,它返回一个包含所有Promise结果的数组,每个结果都是一个对象,包含statusvaluereason属性。

  • status: "fulfilled""rejected",表示Promise的状态。
  • value: 如果Promise成功,则包含Promise的resolve值。
  • reason: 如果Promise失败,则包含Promise的reject原因。

应用场景:

  • 日志记录: 你可以同时向多个日志服务器发送日志,即使某些服务器失败,也不影响其他服务器的日志记录。
  • 数据备份: 你可以同时将数据备份到多个存储服务器,即使某些服务器失败,也不影响数据的完整性。
  • 不关心单个请求结果的场景: 某些情况下,你只想知道所有请求是否都结束了,而不在乎每个请求的具体结果。
function fetchData(url, shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(`从 ${url} 获取数据失败`);
      } else {
        resolve(`从 ${url} 获取的数据`);
      }
    }, 500); // 模拟网络延迟
  });
}

Promise.allSettled([
  fetchData('https://example.com/api1', false),
  fetchData('https://example.com/api2', true),
  fetchData('https://example.com/api3', false),
])
  .then(results => {
    console.log(results);
    // 输出:
    // [
    //   { status: 'fulfilled', value: '从 https://example.com/api1 获取的数据' },
    //   { status: 'rejected', reason: '从 https://example.com/api2 获取数据失败' },
    //   { status: 'fulfilled', value: '从 https://example.com/api3 获取的数据' }
    // ]
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('请求成功:', result.value);
      } else {
        console.error('请求失败:', result.reason);
      }
    });
  });

第四位乐观选手:Promise.any()

Promise.any(),是ES2021新增的方法,它也接收一个Promise数组。 与Promise.all()相反,Promise.any()只要有一个Promise成功,它就会成功,并返回第一个成功的Promise的结果。 如果所有Promise都失败Promise.any()就会失败,并返回一个AggregateError对象,该对象包含所有失败的Promise的reason。

应用场景:

  • 选择最佳服务器: 你可以向多个服务器发送请求,哪个服务器最先返回成功的结果,就使用哪个服务器。 比如,你有多个CDN节点,你想选择速度最快的那个。
  • 容错处理: 你可以尝试使用多个API,只要有一个API成功,就认为操作成功。
function fetchData(url, shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(`从 ${url} 获取数据失败`);
      } else {
        resolve(`从 ${url} 获取的数据`);
      }
    }, 500); // 模拟网络延迟
  });
}

Promise.any([
  fetchData('https://example.com/api1', true),
  fetchData('https://example.com/api2', false),
  fetchData('https://example.com/api3', true),
])
  .then(result => {
    console.log('请求成功:', result); // 输出: 请求成功: 从 https://example.com/api2 获取的数据
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

// 如果所有请求都失败
Promise.any([
    fetchData('https://example.com/api1', true),
    fetchData('https://example.com/api2', true),
  ])
    .then(result => {
      // 不会执行到这里
    })
    .catch(error => {
      console.error('请求全部失败:', error); // 输出 AggregateError: All promises were rejected
      console.log(error.errors); // 输出包含所有reject reason的数组
    });

总结:四兄弟的差异

为了方便大家记忆和区分,我们用一个表格来总结一下这四个Promise方法的差异:

方法 成功条件 失败条件 返回值 应用场景
Promise.all() 所有Promise都成功 任何一个Promise失败 包含所有成功Promise结果的数组 (顺序与输入一致) 并发请求,表单验证等,需要所有操作都成功才能进行下一步的场景
Promise.race() 任何一个Promise完成 (成功或失败) 无 (只关心第一个完成的Promise) 第一个完成的Promise的结果 (成功则resolve的值,失败则reject的原因) 超时控制,备用方案等,只需要最快的结果的场景
Promise.allSettled() 所有Promise都完成 (成功或失败) 无 (总是等待所有Promise完成) 包含所有Promise结果的数组,每个结果包含状态(fulfilled/rejected)和值/原因 日志记录,数据备份等,需要知道所有操作是否都结束的场景,不关心单个操作的结果
Promise.any() 任何一个Promise成功 所有Promise都失败 第一个成功的Promise的结果 选择最佳服务器,容错处理等,只需要一个操作成功即可的场景

一个综合案例:图片加载

假设你需要在网页上加载多张图片,你想同时加载所有图片,但如果其中某张图片加载失败,你不想中断其他图片的加载。 同时,你还想给每张图片设置一个加载超时时间。

function loadImage(url, timeout) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => {
      resolve(img);
    };
    img.onerror = () => {
      reject(`加载图片失败: ${url}`);
    };
    img.src = url;

    // 超时处理
    setTimeout(() => {
      reject(`加载图片超时: ${url}`);
    }, timeout);
  });
}

const imageUrls = [
  'https://example.com/image1.jpg',
  'https://example.com/image2.png',
  'https://example.com/image3.gif',
  'https://example.com/image4.jpeg',
];

const imagePromises = imageUrls.map(url => loadImage(url, 2000)); // 2秒超时

Promise.allSettled(imagePromises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        const img = result.value;
        document.body.appendChild(img); // 将加载成功的图片添加到页面
      } else {
        console.error(`图片 ${imageUrls[index]} 加载失败: ${result.reason}`);
      }
    });
  });

在这个例子中,我们使用了Promise.allSettled()来确保所有图片都会尝试加载,即使某些图片加载失败也不会影响其他图片。 同时,我们在loadImage函数中使用了setTimeout来设置超时时间,如果图片在规定时间内没有加载完成,就会被认为加载失败。

总结与展望

好了,关于Promise.all(), Promise.race(), Promise.allSettled(), 和 Promise.any(),咱们就先聊到这里。 希望通过今天的讲解,大家能够对它们的作用和区别有更清晰的认识,并在实际开发中灵活运用。

记住,理解这些Promise方法,不仅能让你的代码更简洁、更优雅,还能让你更好地处理异步操作,提升应用的性能和用户体验。

当然,Promise的世界还有很多值得探索的地方,比如async/awaitPromise.withResolvers()等等。 以后有机会,咱们再一起深入研究。 感谢各位的收听!

发表回复

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