絕大部分情況,網路請求都是先請求先響應。但是某些情況下,由於未知的一些問題,可能會導致先請求的 api 後返回。最簡單的解決方案就是新增 loading 狀態,在所有請求都完成後才能進行下一次請求。
但不是所有的業務都可以採用這種方式。這時候開發者就需要對其進行處理以避免渲染錯誤資料。
使用“版本號”
我們可以使用版本號來決策業務處理以及資料渲染:
const invariant = (condition: boolean, errorMsg: string) => {
if (condition) {
throw new Error(errorMsg)
}
}
let versionForXXXQuery = 0;
const checkVersionForXXXQuery = (currentVersion: number) => {
// 版本不匹配,就丟擲錯誤
invariant(currentVersion !== versionForXXXQuery, 'The current version is wrong')
}
const XXXQuery = async () => {
// 此處只能使用 ++versionForXXXQuery 而不能使用 versionForXXXQuery++
// 否則版本永遠不對應
const queryVersion = ++versionForXXXQuery;
// 業務請求
checkVersion(queryVersion)
// 業務處理
// ?介面渲染
// 業務請求
checkVersion(queryVersion)
// 業務處理
// ?介面渲染
}
如此,先請求的 api 後返回就會被錯誤中止執行,但最終渲染到介面上的只有最新版本的請求。但是該方案對業務的侵入性太強。雖然我們可以利用 class 和 AOP 來簡程式碼和邏輯。但對於開發來說依舊不友好。這時候我們可以使用 AbortController。
使用 AbortController
AbortController 取消之前請求
話不多說,先使用 AbortController 完成上面相同的功能。
let abortControllerForXXXQuery: AbortController | null = null
const XXXQuery = async () => {
// 當前有中止控制器,直接把上一次取消
if (abortControllerForXXXQuery) {
abortControllerForXXXQuery.abort()
}
// 新建控制器
abortControllerForXXXQuery = new AbortController();
// 獲取訊號
const { signal } = abortControllerForXXXQuery
const resA = await fetch('xxxA', { signal });
// 業務處理
// ?介面渲染
const resB = await fetch('xxxB', { signal });
// 業務處理
// ?介面渲染
}
我們可以看到:程式碼非常簡單,同時得到了效能增強,瀏覽器將提前停止獲取資料(注:伺服器依舊會處理多次請求,只能通過 loading 來降低伺服器壓力)。
AbortController 移除繫結事件
雖然程式碼很簡單,但是為什麼需要這樣新增一個 AbortController 類而不是直接通過新增 api 來進行中止網路請求操作呢?這樣不是增加了複雜度嗎?筆者開始也是這樣認為的。到後面才發現。AbortController 類雖然較為複雜了,但是它是通用的,因此 AbortController 可以被其他 Web 標準和 JavaScript 庫使用。
const controller = new AbortController()
const { signal } = controller
// 新增事件並傳遞 signal
window.addEventListener('click', () => {
console.log('can abort')
}, { signal })
window.addEventListener('click', () => {
console.log('click')
});
// 開始請求並且新增 signal
fetch('xxxA', { signal })
// 移除第一個 click 事件同時中止未完成的請求
controller.abort()
通用的 AbortController
既然它是通用的,那是不是也可以終止業務方法呢。答案是肯定的。先來看看 AbortController 到底為啥能夠通用呢?
AbortController 提供了一個訊號量 signal 和中止 abort 方法,通過這個訊號量可以獲取狀態以及繫結事件。
const controller = new AbortController();
// 獲取訊號量
const { signal } = controller;
// 獲取當前是否已經執行過 abort,目前返回 false
signal.aborted
// 新增事件
signal.addEventListener('abort', () => {
console.log('觸發 abort')
})
// 新增事件
signal.addEventListener('abort', () => {
console.log('觸發 abort2')
})
// 中止 (不可以解構直接執行 abort,有 this 指向問題)
// 控制檯列印 觸發 abort,觸發 abort2
controller.abort()
// 當前是否已經執行過 abort,返回 ture
signal.aborted
// 控制檯無反應
controller.abort();
無疑,上述的事件新增了 abort 事件的監聽。綜上,筆者簡單封裝了一下 AbortController。Helper 類如下所示:
class AbortControllerHelper {
private readonly signal: AbortSignal
constructor(signal: AbortSignal) {
this.signal = signal
signal.addEventListener('abort', () => this.abort())
}
/**
* 執行呼叫方法,只需要 signal 狀態的話則無需在子類實現
*/
abort = (): void => {}
/**
* 檢查當前是否可以執行
* @param useBoolean 是否使用布林值返回
* @returns
*/
checkCanExecution = (useBoolean: boolean = false): boolean => {
const { aborted } = this.signal
// 如果使用布林值,返回是否可以繼續執行
if (useBoolean) {
return !aborted
}
// 直接丟擲異常
if (aborted) {
throw new Error('abort has already triggered');
}
return true
}
}
如此,開發者可以新增子類繼承 AbortControllerHelper 並放入 signal。然後通過一個 AbortController 中止多個乃至多種不同事件。
鼓勵一下
如果你覺得這篇文章不錯,希望可以給與我一些鼓勵,在我的 github 部落格下幫忙 star 一下。