如何在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'