你真的該在 JavaScript 里“多取消”
某天我在做一個(gè) Web 應(yīng)用:三個(gè) fetch 并發(fā),用戶還沒等結(jié)果出來就切走了頁面。然后呢?這三條請(qǐng)求仍舊跑到完結(jié)——帶寬被浪費(fèi)、CPU 被占用、用戶也得不到任何好處。
那一刻我意識(shí)到:異步任務(wù)的“取消”,不是可選項(xiàng),而是必選項(xiàng)。 你是否也經(jīng)歷過這種“用戶一走了之、網(wǎng)絡(luò)卻在后臺(tái)堵車”的場景?
我需要一個(gè)能及時(shí)叫停異步操作的機(jī)制。
接下來看看這個(gè)很酷的 JavaScript 能力,如何把異步的生殺大權(quán)交回到你手里——因此應(yīng)用更快、同時(shí)用戶更開心、而且服務(wù)器壓力更小。
機(jī)制揭秘:它是如何工作的
主角叫 AbortController。這是控制異步取消的原生 API。
它的思路非常樸素:創(chuàng)建一個(gè) controller,拿到其中的 signal,把這個(gè)信號(hào)傳給需要“可取消”的異步操作(例如 fetch,也可以是你封裝的自定義異步函數(shù),甚至是計(jì)時(shí)器)。當(dāng)你在合適的時(shí)機(jī)調(diào)用 abort(),所有與該 signal 關(guān)聯(lián)的任務(wù)都會(huì)被終止。
一個(gè)極簡示例:
const controller = new AbortController();
const { signal } = controller;
fetch('/api/data', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch canceled');
}
});
// 稍后……
controller.abort(); // 取消該次請(qǐng)求signal 把 fetch 和控制器綁在一起;abort() 一下,立刻“掐斷”。因此,當(dāng)用戶已離場,我們就不必讓請(qǐng)求在后臺(tái)無意義地跑完。
真實(shí)場景:搜索請(qǐng)求的“競態(tài)清除”
你可能寫過即時(shí)搜索:用戶每敲一個(gè)字符就發(fā)一次請(qǐng)求。如果不取消舊請(qǐng)求,競態(tài)隨時(shí)發(fā)生——“a”的響應(yīng)可能晚于“abc”,最后把正確結(jié)果又覆蓋掉了。
用 AbortController,每次新請(qǐng)求發(fā)出時(shí)先取消舊的:
let controller = null;
function search(query) {
// 1) 取消上一次未完成的請(qǐng)求
if (controller) controller.abort();
// 2) 為本次查詢創(chuàng)建新控制器
controller = new AbortController();
fetch(`https://api.example.com/search?q=${query}`, { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Results:', data))
.catch(err => {
if (err.name === 'AbortError') return; // 預(yù)期中的取消,不算錯(cuò)誤
console.error('Error:', err);
});
}
// 連續(xù)輸入的模擬
search('cat');
search('cats'); // 'cat' 的請(qǐng)求被取消,只保留最新的結(jié)果呢?只有最新那次查詢會(huì)落地,因此后端不再被“鍵盤風(fēng)暴”轟炸,同時(shí)前端也能避免錯(cuò)亂刷新,從而讓列表穩(wěn)定且準(zhǔn)確。
不止網(wǎng)絡(luò):計(jì)時(shí)器同樣可控
AbortController絕不是fetch 的專屬。事實(shí)上,你可以把它接到幾乎任何異步流程上。下面是一個(gè)“用戶離開就掐掉超時(shí)器”的小例子:
const controller = new AbortController();
const { signal } = controller;
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(new Error("Timeout canceled"));
});
});
}
delay(5000, signal)
.then(() => console.log("Done"))
.catch(console.error);
// 在 5 秒之前取消
controller.abort();這樣一來,不再需要那些“沒人等、卻一直掛著”的計(jì)時(shí)器;因此副作用更少,而且可回收性更好。
為什么這件事如此重要?
使用 AbortController 會(huì)讓你的前端更“干凈”:
- 性能:殺掉沒人需要的請(qǐng)求;因此節(jié)省帶寬與 CPU。
- 體驗(yàn):不再閃爍、不再“過期數(shù)據(jù)”覆蓋新結(jié)果;同時(shí)加載狀態(tài)可控。
- 可控性:由你決定哪些異步繼續(xù)、哪些該終止——從而避免連鎖反應(yīng)與資源泄漏。
但也別忽略它的邊界
- 舊瀏覽器兼容性:在 2018 年之前的環(huán)境里,
AbortController支持并不理想;因此需要 polyfill 或降級(jí)策略。 - 部分 API 不支持
signal:例如某些老的 WebSocket 實(shí)現(xiàn)與第三方 SDK;不過你仍可以在外層封裝一個(gè)“軟取消”(即忽略結(jié)果)來達(dá)成類似效果。 - 團(tuán)隊(duì)規(guī)范:取消語義需要貫穿調(diào)用棧;否則容易出現(xiàn)“上層以為取消了、下層仍在跑”的錯(cuò)位——因此請(qǐng)為自定義異步函數(shù)設(shè)計(jì)
signal參數(shù)并妥善傳播。
最后的結(jié)論
我曾經(jīng)把無用請(qǐng)求當(dāng)成 Web 的“自然損耗”。而現(xiàn)在,只要是交互密集的頁面,我幾乎不會(huì)再寫沒有“取消通道”的異步。
試著在下一個(gè)項(xiàng)目里用用 AbortController:因此你的網(wǎng)絡(luò)更清爽,同時(shí)用戶看到的內(nèi)容更穩(wěn)定,最終你的應(yīng)用會(huì)顯得更專業(yè)。


























