1. AbortController 可以复用吗?
答:不能。一旦一个 signal 被 abort
他的状态就永远是 aborted=true
也就不会再触发 abort
事件。
有两种解决办法:
"全局"变量
tsx
// 初始化一次
let abortController = new AbortController()
export default ({ question }) => {
const handleCancel = () => {
abortController.abort()
// 每次 abort 后再初始化一次
abortController = new AbortController()
};
const startChat = () => {
request(
{ message: question, signal: abortController.signal },
{
onUpdate: (partialAnswer) => {
setAnswer(partialAnswer)
},
onSuccess: () => {
setThinkingStatus('Completed')
setThinkingStopTime(Date.now())
},
},
)
}
}
ref
来保存引用,否则每次 rerender 都会实例化一个新的 AbortController 导致无法中断。
tsx
export default ({ question }) => {
// 初始化一次
const abortControllerRef = React.useRef<AbortController>()
const handleCancel = () => {
abortController.abort()
// 每次 abort 后再初始化一次
abortControllerRef = new AbortController()
}
startChat() {
request(
{ message: question, signal: abortControllerRef.signal },
{
onUpdate: (partialAnswer) => {
setAnswer(partialAnswer)
},
onSuccess: () => {
setThinkingStatus('Completed')
setThinkingStopTime(Date.now())
},
},
)
}
}
}
2. AbortController 能一次取消多个 fetch 请求吗?
答:能。注意是一次取消不是连续取消,和 1 并不冲突。因为背后的原理是 fetch 内部会监听来自同一个 abortController 信号的 abort 事件,也就是 abort 事件是"广播"可以被多个消费者感知。
ts
let urls = [...]; // a list of urls to fetch in parallel
let controller = new AbortController();
// an array of fetch promises
let fetchJobs = urls.map(url => fetch(url, {
signal: controller.signal
}));
let results = await Promise.all(fetchJobs);
// if controller.abort() is called from anywhere,
// it aborts all fetches
我们验证下:
从截图可以还可以看出 abort 事件回调是同步的。
3. 一个请求能被多个 AbortController 取消吗?
fetch 虽然只能接受一个 signal,但是我们可以利用 AbortSignal.any
这个静态方法传入 signal 数组。 只要其中之一 abort 则结果为 aborted。
ts
const cancelDownloadButton = document.getElementById("cancelDownloadButton");
const userCancelController = new AbortController();
cancelDownloadButton.addEventListener("click", () => {
userCancelController.abort();
});
// 创建 Timeout controller 实例的快捷方式,下文会介绍
const timeoutSignal = AbortSignal.timeout(1_000 * 60 * 5);
// 取决于用户取消和超时二者哪个快
const combinedSignal = AbortSignal.any([
userCancelController.signal,
timeoutSignal,
]);
try {
const res = await fetch(someUrlToDownload, {
signal: combinedSignal,
});
} catch (e) {
if (e.name === "AbortError") {
// 用户取消
} else if (e.name === "TimeoutError") {
// 超时自动取消
} else {
// ...
}
}
4. AbortController 除了能被 fetch 当做参数,还可以被哪些原生 API 当做参数?
在 fetch 中使用略过
ts
const controller = new AbortController();
const signal = controller.signal;
// Start fetch
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
// On abort, the promise is rejected with an AbortError
if (err.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error('Another error', err);
}
});
// Abort fetch after 2 seconds
setTimeout(() => controller.abort(), 2000);
addEventListener
ts
// Create a new AbortController
const controller = new AbortController();
// Get the AbortSignal from the controller
const signal = controller.signal;
// Listen for click events on the document
document.addEventListener('click', () => {
console.log('Document was clicked');
}, { signal });
// Call abort on the controller after 5 seconds
setTimeout(() => {
controller.abort();
console.log('No longer listening for clicks');
}, 5e3);
上述代码的效果是,五秒后 document 点击不生效,自动注销 click 回调,即被 abort 后,document 的 click event listener 数组长度将减一。相当于可控制的 once。
其他:Node.js 的流中。此处不表。
5. 创建超时自动 abort 的快捷静态方法 AbortSignal.timeout
AbortSignal.timeout(time)
将返回一个超时自动 abort 的实例。
使用 AbortSignal.timeout
简化版:
ts
const signal = AbortSignal.timeout(5e3);
document.addEventListener('click', () => {
console.log('Document was clicked');
}, { signal });
相比 setTimeout 手动 abort,该方法有个缺点无法在超时时做一些业务逻辑,因为无法监听 timeout
事件。