JS `Promise.race()`:并发执行多个 Promise 并获取第一个完成的结果

各位观众,晚上好!我是今晚的主讲人,咱们今天来聊聊 JavaScript 里一个非常有意思的函数:Promise.race()。 保证让大家听得明白,用得溜!

开场白:龟兔赛跑的 Promise 世界

大家小时候都听过龟兔赛跑的故事吧?兔子自恃跑得快,中途睡大觉,结果被乌龟超了。Promise.race() 就有点像这个故事,它让一堆 Promise 赛跑,但它只关心谁第一个跑完,至于其他的,它根本不在乎!

什么是 Promise.race()

Promise.race() 是 JavaScript 中 Promise 对象的一个静态方法。它的作用是:

  • 接收一个包含多个 Promise 对象的数组(或者任何可迭代对象)。
  • 并发地执行这些 Promise
  • 一旦其中一个 Promise 完成(fulfilled 或 rejected),Promise.race() 返回的 Promise 也会立即以相同的结果(value 或 reason)完成。

简单来说:谁跑得最快,就听谁的!

语法:

Promise.race(promises);
  • promises: 一个可迭代对象,比如数组,其中包含 Promise 对象。

返回值:

  • 一个新的 Promise 对象。这个 Promise 的状态和值/原因取决于输入数组中第一个完成的 Promise。

举个栗子:Promise.race() 的初体验

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 完成!');
  }, 2000); // 2秒后完成
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 2 失败!');
  }, 1000); // 1秒后失败
});

Promise.race([promise1, promise2])
  .then(result => {
    console.log('最终结果:', result); // 输出: 最终结果: Promise 2 失败!
  })
  .catch(error => {
    console.error('出现错误:', error); // 输出: 出现错误: Promise 2 失败!
  });

// 注意:这里promise2先reject,所以会直接进入catch

在这个例子中,promise2promise1 先完成(reject)。因此,Promise.race() 返回的 Promise 也会立即以 promise2 的结果(reject)完成。也就是说,promise1 辛辛苦苦跑完,也没人理它,因为 promise2 已经赢了!

Promise.race() 的工作原理:深入解析

  1. 并发执行: Promise.race() 会立即并发地执行传入的 Promise 数组中的所有 Promise。这意味着它们会同时开始运行,而不是一个接一个地执行。

  2. 监听状态变化: Promise.race() 会监听每个 Promise 的状态变化(fulfilled 或 rejected)。

  3. 第一个完成者胜出: 一旦其中一个 Promise 完成(无论是 fulfilled 还是 rejected),Promise.race() 就会立即采用该 Promise 的状态和值/原因。

  4. 忽略其他 Promise: 在第一个 Promise 完成后,Promise.race() 会忽略数组中所有其他 Promise 的结果。即使其他 Promise 后来也完成了,Promise.race() 也不会再改变它的状态。

用表格来总结一下:

特性 描述
并发执行 所有 Promise 同时开始运行。
状态监听 监听每个 Promise 的 fulfilled 或 rejected 状态。
胜出条件 第一个完成(fulfilled 或 rejected)的 Promise 决定了 Promise.race() 的结果。
忽略其他 Promise 一旦有 Promise 胜出,其他 Promise 的结果将被忽略。

Promise.race() 的常见应用场景:

Promise.race() 在实际开发中有很多用处,下面列举几个常见的场景:

  1. 超时控制:

    有时候,我们需要对某个操作设置一个超时时间。如果操作在规定时间内没有完成,就认为它失败了。Promise.race() 可以很方便地实现这个功能。

    function withTimeout(promise, timeout) {
      const timeoutPromise = new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('操作超时!');
        }, timeout);
      });
    
      return Promise.race([promise, timeoutPromise]);
    }
    
    // 模拟一个需要很长时间才能完成的 Promise
    const longRunningPromise = new Promise(resolve => {
      setTimeout(() => {
        resolve('耗时操作完成!');
      }, 3000);
    });
    
    withTimeout(longRunningPromise, 2000) // 设置 2 秒超时
      .then(result => {
        console.log('结果:', result);
      })
      .catch(error => {
        console.error('错误:', error); // 输出: 错误: 操作超时!
      });

    在这个例子中,withTimeout 函数接收一个 Promise 和一个超时时间。它创建一个新的 Promise timeoutPromise,在指定的时间后 reject。然后,使用 Promise.race() 将原始 Promise 和 timeoutPromise 放在一起赛跑。如果原始 Promise 在超时时间内没有完成,timeoutPromise 就会先 reject,从而导致整个操作失败。

  2. 服务降级:

    在高并发的场景下,某些服务可能会出现性能瓶颈。为了保证系统的可用性,我们可以使用 Promise.race() 实现服务降级。

    async function fetchFromServiceA() {
      // 尝试从服务 A 获取数据
      try {
        const response = await fetch('https://service-a.example.com/data');
        return await response.json();
      } catch (error) {
        console.error('服务 A 失败:', error);
        return null;
      }
    }
    
    async function fetchFromServiceB() {
      // 尝试从服务 B 获取数据(备用服务)
      try {
        const response = await fetch('https://service-b.example.com/data');
        return await response.json();
      } catch (error) {
        console.error('服务 B 失败:', error);
        return null;
      }
    }
    
    async function getData() {
      const data = await Promise.race([fetchFromServiceA(), fetchFromServiceB()]);
    
      if (data) {
        return data;
      } else {
        // 如果两个服务都失败了,返回默认数据或者抛出错误
        return { message: '所有服务均不可用,返回默认数据' };
      }
    }
    
    getData().then(data => {
      console.log('获取到的数据:', data);
    });

    在这个例子中,我们首先尝试从服务 A 获取数据。如果服务 A 失败了,就尝试从服务 B 获取数据。Promise.race() 保证了我们总是能够尽快地得到结果,即使某个服务出现了问题。

  3. 竞态条件处理:

    在某些情况下,多个操作可能会竞争同一个资源。Promise.race() 可以用来处理这种竞态条件。

    let resourceLocked = false;
    
    async function acquireResource() {
      return new Promise((resolve, reject) => {
        if (!resourceLocked) {
          resourceLocked = true;
          resolve('成功获取资源!');
        } else {
          reject('资源已被占用!');
        }
      });
    }
    
    async function operation1() {
      try {
        const result = await acquireResource();
        console.log('操作 1:', result);
        // 模拟使用资源
        await new Promise(resolve => setTimeout(resolve, 1000));
        resourceLocked = false; // 释放资源
      } catch (error) {
        console.error('操作 1:', error);
      }
    }
    
    async function operation2() {
      try {
        const result = await acquireResource();
        console.log('操作 2:', result);
        // 模拟使用资源
        await new Promise(resolve => setTimeout(resolve, 500));
        resourceLocked = false; // 释放资源
      } catch (error) {
        console.error('操作 2:', error);
      }
    }
    
    Promise.race([operation1(), operation2()]).then(() => {
      console.log("其中一个操作已经完成,无论成功与否");
    });

    在这个例子中,acquireResource 函数尝试获取一个共享资源。如果资源已经被占用,函数会 reject。operation1operation2 都会尝试获取资源。Promise.race() 确保只有一个操作能够成功获取资源,另一个操作会失败。

  4. 取消操作(高级用法):

    虽然 Promise.race() 本身不能直接取消 Promise,但我们可以结合 AbortController 来实现取消操作的效果。

    const controller = new AbortController();
    const signal = controller.signal;
    
    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/data', { signal });
        return await response.json();
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Fetch 操作被取消!');
        } else {
          console.error('Fetch 操作失败:', error);
        }
        return null;
      }
    }
    
    const timeoutPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        controller.abort(); // 取消 fetch 操作
        reject('请求超时!');
      }, 2000);
    });
    
    Promise.race([fetchData(), timeoutPromise])
      .then(data => {
        if (data) {
          console.log('获取到的数据:', data);
        }
      })
      .catch(error => {
        console.error('错误:', error);
      });
    
    // 在 1 秒后取消操作 (模拟用户取消操作)
    setTimeout(() => {
      controller.abort();
    }, 1000);

    在这个例子中,我们使用 AbortController 来控制 fetch 操作。timeoutPromise 在 2 秒后会调用 controller.abort() 取消 fetch 操作。Promise.race() 确保如果 fetch 操作超时,就会被取消。

注意事项:

  • 空数组: 如果传递给 Promise.race() 的数组为空,它将返回一个永远 pending 的 Promise。

    Promise.race([])
      .then(() => {
        console.log('永远不会执行到这里');
      })
      .catch(() => {
        console.log('永远不会执行到这里');
      });
  • 非 Promise 值: 如果数组中包含非 Promise 值,这些值会被转换为 Promise。

    Promise.race([1, Promise.resolve(2), 'hello'])
      .then(result => {
        console.log('结果:', result); // 输出: 结果: 1
      });
  • 错误处理: 记得使用 .catch() 来处理 Promise.race() 返回的 Promise 的 rejection 情况。

Promise.race() 的一些高级技巧:

  1. 动态添加 Promise: 虽然 Promise.race() 接收的是一个数组,但我们可以利用 JavaScript 的灵活性,动态地向数组中添加 Promise。

    const promises = [];
    
    promises.push(new Promise(resolve => setTimeout(() => resolve('Promise A'), 1500)));
    promises.push(new Promise(resolve => setTimeout(() => resolve('Promise B'), 1000)));
    
    Promise.race(promises)
      .then(result => console.log('结果:', result)); // 输出: 结果: Promise B
    
    // 稍后添加一个更快的 Promise
    setTimeout(() => {
      promises.push(new Promise(resolve => setTimeout(() => resolve('Promise C'), 500)));
    }, 200); // 稍等片刻,确保前两个 Promise 已经开始执行

    这个例子展示了如何动态地向 promises 数组中添加 Promise。需要注意的是,如果后续添加的 Promise 完成速度更快,它将胜出。

  2. 结合 async/await Promise.race() 可以与 async/await 结合使用,使代码更加简洁易读。

    async function runRace() {
      try {
        const result = await Promise.race([
          new Promise(resolve => setTimeout(() => resolve('Promise 1'), 1000)),
          new Promise(reject => setTimeout(() => reject('Promise 2'), 500)),
        ]);
        console.log('结果:', result);
      } catch (error) {
        console.error('错误:', error); // 输出: 错误: Promise 2
      }
    }
    
    runRace();

    在这个例子中,async/await 使得 Promise.race() 的使用更加直观。

总结:

Promise.race() 是一个强大的工具,可以用来解决各种并发问题。通过合理地运用它,我们可以提高程序的性能、可靠性和用户体验。

最后,再用一句话总结: Promise.race() 就像一场比赛,只有最快的选手才能赢得奖牌!希望大家以后在开发中能灵活使用 Promise.race(),让你的代码跑得更快,更稳定!

今天的讲座就到这里,谢谢大家! 如果大家有什么问题,欢迎提问。

发表回复

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