目录
方法 1:使用新的 Promise.withResolvers()
方法 2:使用 AbortController
在 JavaScript 中,你可能已经知道如何取消请求:对于 XHR 可以使用 xhr.abort()
,对于 fetch 可以使用 signal 。但是你如何取消一个普通的 Promise 呢?
目前,JavaScript 的 Promise 本身并不提供取消常规 Promise 的 API。因此,我们接下来要讨论的是如何丢弃 / 忽略 Promise 的结果。
方法 1:使用新的 Promise.withResolvers()
之前写法
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
现在我们可以
const { promise, resolve, reject } = Promise.withResolvers();
因此我们可以利用这一点来暴露一个名为 “cancel” 的方法:
const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
let rejected = false;
const { promise, resolve, reject } = Promise.withResolvers<T>();
return {
run: () => {
if (!rejected) {
asyncFn().then(resolve, reject);
}
return promise;
},
cancel: () => {
rejected = true;
reject(new Error('CanceledError'));
},
};
};
测试代码
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
const ret = buildCancelableTask(async () => {
await sleep(1000);
return 'Hello';
});
(async () => {
try {
const val = await ret.run();
console.log('val: ', val);
} catch (err) {
console.log('err: ', err);
}
})();
setTimeout(() => {
ret.cancel();
}, 500);
这里,将任务预设为至少耗时1000毫秒,但我们在接下来的500毫秒内取消了任务,所以会看到:
注意,这并不是真正的取消,而是提前拒绝。原来的 asyncFn()
将继续执行,直到解析或拒绝为止,但这并不重要,因为用 Promise.withResolvers<T>()
创建的 Promise 已经被拒绝了。
方法 2:使用 AbortController
就像我们取消获取请求一样,我们也可以实现一个监听器来实现提前拒绝。它看起来是这样的:
const buildCancelableTask = <T>(asyncFn: () => Promise<T>) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
const cancelTask = () => reject(new Error('CanceledError'));
if (abortController.signal.aborted) {
cancelTask();
return;
}
asyncFn().then(resolve, reject);
abortController.signal.addEventListener('abort', cancelTask);
}),
cancel: () => {
abortController.abort();
},
};
};
它具有与上述相同的效果,但使用的是 AbortController。你可以在此处使用其他监听器,但 AbortController 提供了额外好处,即如果你多次调用 cancel ,它不会多次触发 'abort' 事件。
基于此代码,我们可以进一步构建可取消的获取操作。这在需要连续请求的场景下非常有用,例如您可能希望丢弃之前的请求结果并使用最新的请求结果。
const buildCancelableFetch = <T>(
requestFn: (signal: AbortSignal) => Promise<T>,
) => {
const abortController = new AbortController();
return {
run: () =>
new Promise<T>((resolve, reject) => {
if (abortController.signal.aborted) {
reject(new Error('CanceledError'));
return;
}
requestFn(abortController.signal).then(resolve, reject);
}),
cancel: () => {
abortController.abort();
},
};
};
const ret = buildCancelableFetch(async signal => {
return fetch('http://localhost:5000', { signal }).then(res =>
res.text(),
);
});
(async () => {
try {
const val = await ret.run();
console.log('val: ', val);
} catch (err) {
console.log('err: ', err);
}
})();
setTimeout(() => {
ret.cancel();
}, 500);
请注意,这不会影响服务器处理逻辑;它只会导致浏览器拒绝 / 取消请求,换句话说,如果你发送 POST 请求以更新用户信息,它仍然可能生效。因此,这种方法更常用于在获取新数据时发送 GET 请求的场景。