Fetch: AbortController
Abort not only fetch
, but other asynchronous tasks as well.
Data fetching 系列文
1. Data fetching and performance
2. Race condition
3. Fetch: AbortController
🔖 文章索引
1. The AbortController object
2. Using with fetch
3 In react
4. AbortController is scalable
AbortController 是原生的 JS Class,做的事就跟他的名字 Abort 一樣: 放棄已經開始的任務。這點在 asynchronous task 特別好用,因為原生 fetch 並沒有中途放棄的 method,所以恰好可以搭配 AbortController 。
The AbortController object
這個 controller 很簡單,有一個 abort
的 method,以及 signal
property。當 abort()
被呼叫時
signal
觸發 abort eventsignal.aborted
從false
→true
signal.reason
則從undefined
→ error object ,當然你也可以自訂它
let controller = new AbortController();
// controller.abort() 執行後會觸發 controller.signal 的 abort event
controller.signal.addEventListener('abort', () => {
alert(controller.signal.reason)); // user cancellation
}
controller.abort('user cancellation'); // abort!
alert(controller.signal.aborted); // true
甚至可以運用在各種 event listeners 上,以前需要個別根據不同 listener removeEventListener()
但現在 single AbortController 就可以全部一次取消。
useEffect(() => {
const controller = new AbortController()
window.addEventListener('resize', handleResize, {
signal: controller.signal,
})
window.addEventListener('hashchange', handleHashChange, {
signal: controller.signal,
})
window.addEventListener('storage', handleStorageChange, {
signal: controller.signal,
})
return () => {
// Calling `.abort()` removes ALL event listeners
// associated with `controller.signal`. Gone!
controller.abort()
}
}, [])
Using with fetch
為了讓 fetch 可以中止 async task,需要把 signal
property 放到 fetch option,這樣 fetch
就可以藉由 signal
listen abort
這個 event
let controller = new AbortController();
fetch(url, {
signal: controller.signal
});
當執行 controller.abort()
, fetch 藉由signal
listen 到 abort
就會取消 請求,程式也會跳到 reject,所以可以在 catch 裡捕捉到 AbortError
// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
let response = fetch('/article/fetch-abort/demo/hang', {
signal: controller.signal
})
.catch(err => {
if (err.name == 'AbortError') {
// handle abort()
} else {
throw err;
}
});
實際例子
- Upload 一個檔案 (POST /upload),但當使用者按下 Cancel 就要中止上傳
In react
在中大型 React 專案中,若使用原生 fetch 來 request,可能會遇到 Race Condition 問題 (詳細例子會在 Race conditions 這篇講),而 React 每一次 render function 都是獨立的,所以常會發生難以預期結果,這時就很適合在 cleanup 時把此次的 fetch request 取消。
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then((r) => // xxx)
return () => {
// abort the request here
controller.abort();
};
}, [url]);
AbortSignal.timeout
timeout
這個屬性可以讓你甚至不用 AbortController
就可以做到若多久沒有成功就放棄 request
fetch(url, {
// Abort this request automatically if it takes
// more than 3000ms to complete.
signal: AbortSignal.timeout(3000),
})
AbortController is scalable
AbortController
也可以允許同時取消多個 async tasks,如以下範例,並且single controller 就可以做到
let urls = [...]; // a list of urls to fetch in parallel
let controller = new AbortController();
let ourJob = new Promise((resolve, reject) => { // our task
...
controller.signal.addEventListener('abort', reject);
});
// an array of fetch promises
let fetchJobs = urls.map(url => fetch(url, {
signal: controller.signal
}));
// Wait for fetches and our task in parallel
let results = await Promise.all([...fetchJobs, ourJob]);
// if controller.abort() is called from anywhere,
// it aborts all fetches and ourJob