如何在JavaScript中给Promise添加超时?

在过去的许多时候,我发现自己需要在JavaScript中给Promise添加超时。setTimeout()并不完全是这个任务的完美工具,但将其包装成一个Promise相对容易:

const awaitTimeout = delay =>
  new Promise(resolve => setTimeout(resolve, delay));

awaitTimeout(300).then(() => console.log('Hi'));
// 在300毫秒后输出'Hi'

const f = async () => {
  await awaitTimeout(300);
  console.log('Hi');  // 在300毫秒后输出'Hi'
};

这段代码示例并没有特别复杂的地方。它只是使用Promise构造函数将setTimeout()包装起来,并在delay毫秒后解析Promise。当某些代码需要暂停一段时间时,这可能是一个有用的工具。

然而,为了给另一个Promise添加超时,这个工具还需要满足两个额外的需求。第一个需求是允许超时Promise在提供第二个参数作为原因时拒绝而不是解析。另一个需求是创建一个包装函数,用于给Promise添加超时:

const awaitTimeout = (delay, reason) =>
  new Promise((resolve, reject) =>
    setTimeout(
      () => (reason === undefined ? resolve() : reject(reason)),
      delay
    )
  );

const wrapPromise = (promise, delay, reason) =>
  Promise.race([promise, awaitTimeout(delay, reason)]);

wrapPromise(fetch('https://cool.api.io/data.json'), 3000, {
  reason: 'Fetch timeout',
})
  .then(data => {
    console.log(data.message);
  })
  .catch(data => console.log(`Failed with reason: ${data.reason}`));
// 如果`fetch`在3000毫秒内完成,将输出`message`
// 否则,将输出带有原因'Fetch timeout'的错误消息

如您在此示例中所见,reason 用于确定超时 Promise 是解决还是拒绝。然后,awaitTimeout() 用于创建一个新的 Promise,并与其他 Promise 一起传递给 Promise.race() 以创建超时。

这个实现肯定有效,但我们可以进一步改进。一个明显的改进是添加一种清除超时的方法,这需要存储任何活动超时的 id。这个需求以及使这个实用程序自包含的需要都很适合使用 class

class Timeout {
  constructor() {
    this.ids = [];
  }

  set = (delay, reason) =>
    new Promise((resolve, reject) => {
      const id = setTimeout(() => {
        if (reason === undefined) resolve();
        else reject(reason);
        this.clear(id);
      }, delay);
      this.ids.push(id);
    });

  wrap = (promise, delay, reason) =>
    Promise.race([promise, this.set(delay, reason)]);

  clear = (...ids) => {
    this.ids = this.ids.filter(id => {
      if (ids.includes(id)) {
        clearTimeout(id);
        return false;
      }
      return true;
    });
  };
}

const myFunc = async () => {
  const timeout = new Timeout();
  const timeout2 = new Timeout();
  timeout.set(6000).then(() => console.log('Hello'));
  timeout2.set(4000).then(() => console.log('Hi'));
  timeout
    .wrap(fetch('https://cool.api.io/data.json'), 3000, {
      reason: 'Fetch timeout',
    })
    .then(data => {
      console.log(data.message);
    })
    .catch(data => console.log(`Failed with reason: ${data.reason}`))
    .finally(() => timeout.clear(...timeout.ids));
};
// 在 3000ms 后,将记录 `message` 或记录一个 'Fetch timeout' 错误
// 6000ms 的超时将在触发之前被清除,因此不会记录 'Hello'
// 4000ms 的超时不会被清除,因此会记录 'Hi'