如何處理前端開(kāi)發(fā)中的競(jìng)態(tài)請(qǐng)求
點(diǎn)擊上方 前端陽(yáng)光 ,關(guān)注公眾號(hào)
回復(fù) 加群 ,加入技術(shù)交流群交流群
前言
競(jìng)態(tài)條件(Race Conditions)在前端開(kāi)發(fā)中是一種常見(jiàn)的問(wèn)題,特別是在多個(gè)異步操作同時(shí)競(jìng)爭(zhēng)資源或執(zhí)行時(shí)。這可能導(dǎo)致意外的結(jié)果,如數(shù)據(jù)不一致、重復(fù)請(qǐng)求和UI錯(cuò)誤。
舉個(gè)??
一個(gè)最常見(jiàn)的場(chǎng)景就是選項(xiàng)卡切換,用戶(hù)快速切換Tab,由于網(wǎng)絡(luò)延遲等原因,很可能最后停留的Tab所展示的內(nèi)容,并不是用戶(hù)想要看到的內(nèi)容
出現(xiàn)這種情況是因?yàn)?,用?hù)最先點(diǎn)擊的Tab請(qǐng)求把最后點(diǎn)擊的Tab請(qǐng)求覆蓋了(由于網(wǎng)絡(luò)原因,最先發(fā)出的請(qǐng)求最后到達(dá)):
解決方案
取消請(qǐng)求
通常我們可以在新的請(qǐng)求發(fā)起之前,將舊的、未到達(dá)的請(qǐng)求給取消掉,這樣舊的請(qǐng)求就不會(huì)覆蓋新的請(qǐng)求了
XMLHttpRequest 取消請(qǐng)求
XMLHttpRequest(XHR)是一個(gè)內(nèi)建的瀏覽器對(duì)象,它允許使用 JavaScript 發(fā)送 HTTP 請(qǐng)求。
如果請(qǐng)求已被發(fā)出,可以使用 abort() 方法立刻中止請(qǐng)求。
const xhr= new XMLHttpRequest();
xhr.open('GET', 'https://xxx');
xhr.send();
// 取消請(qǐng)求
xhr.abort();
Fetch API 取消請(qǐng)求
fetch 號(hào)稱(chēng)是 AJAX 的替代品,出現(xiàn)于 ES6,它也可以發(fā)出類(lèi)似 XMLHttpRequest 的網(wǎng)絡(luò)請(qǐng)求。
主要的區(qū)別在于 fetch 使用了 Promise,要中止 fetch 發(fā)出的請(qǐng)求,需要使用 AbortController。
const controller = new AbortController();
const signal = controller.signal;
fetch('/xxx', {
signal,
}).then(function(response) {
//...
});
// 取消請(qǐng)求
controller.abort();
Axios cancel Token 取消請(qǐng)求
相比原生 API,大多項(xiàng)目都會(huì)選擇 axios 進(jìn)行請(qǐng)求。
import Axios from 'axios'
const CancelToken = Axios.CancelToken
let cancel
export const getSomeResource = (params: GetSomeResourceReq) => {
if (cancel) {
cancel()
}
const res = Axios.post('/xxx', params)
return res.catch((err) => { // 取消了axios請(qǐng)求會(huì)走到異常處理,我們需要對(duì)這種錯(cuò)誤進(jìn)行過(guò)濾
if (Axios.isCancel(err)) {
return {} as GetSomeResourceRsp
}
throw err
})
}
忽略請(qǐng)求
相較于取消請(qǐng)求,忽略請(qǐng)求更為通用。我們只需要關(guān)注我們最后一次請(qǐng)求的結(jié)果,如果某次請(qǐng)求的返回結(jié)果并不是最新的,那么我們就忽略掉這個(gè)請(qǐng)求。忽略請(qǐng)求的一個(gè)精髓在于終止Promise的響應(yīng),這也是字節(jié)面試官經(jīng)常問(wèn)到的一個(gè)問(wèn)題,我們需要做的就是返回一個(gè)Pending的Promise,從而做到終止的效果 使用鎖標(biāo)記
這里的鎖只的是某個(gè)唯一標(biāo)識(shí),通過(guò)這個(gè)標(biāo)識(shí)來(lái)對(duì)比當(dāng)前請(qǐng)求是否過(guò)期,如下文的prevTimestamp
我們通過(guò)閉包,實(shí)現(xiàn)對(duì)prevTimestamp的緩存,從而做到對(duì)每次請(qǐng)求的返回進(jìn)行對(duì)比的效果
/**
* 處理競(jìng)態(tài)請(qǐng)求
*
* @export
* @param {(...params: any[]) => Promise<any>} fn
* @return {*}
*/
export default function useFetch<T, P>(fn: (...params: [P, ...any[]]) => Promise<T>) {
let prevTimestamp
return function (params: P, ...rest: any[]): Promise<T> {
return new Promise((resolve, reject) => {
const curTimestamp = prevTimestamp = Date.now()
const context = this
fn.call(context, params, ...rest)
.then(res => {
// 只處理最新請(qǐng)求的返回
if (curTimestamp === prevTimestamp) {
resolve(res)
}
}).catch(err => {
// 只處理最后一次請(qǐng)求的異常
if (curTimestamp === prevTimestamp) {
reject(err)
}
})
})
}
}
在使用的時(shí)候,我們就可以用這個(gè)方法把真正要發(fā)出的請(qǐng)求包一下(針對(duì)某一個(gè)具體的原子請(qǐng)求,從而做到多請(qǐng)求皆可竟態(tài)處理)
import Axios from 'axios'
const _getSomeResource = (params: GetSomeResourceReq) => {
return Axios.post('/xxx', params)
}
export const getSomeResource = useFetch(_getSomeResource)
使用隊(duì)列
與使用鎖類(lèi)似,我們把每個(gè)請(qǐng)求都放進(jìn)隊(duì)列里,對(duì)每次返回的請(qǐng)求進(jìn)行判斷,如果這個(gè)請(qǐng)求不是最新的請(qǐng)求,那么就忽略掉
/**
* 處理競(jìng)態(tài)請(qǐng)求
*
* @export
* @param {(...params: any[]) => Promise<any>} fn
* @return {*}
*/
export default function useFetch<T, P>(fn: (...params: [P, ...any[]]) => Promise<T>) {
const queue = []
return function (params: P, ...rest: any[]): Promise<T> {
const p = new Promise((resolve, reject) => {
const context = this
fn.call(context, params, ...rest)
.then(res => {
const isLatest = queue[queue.length - 1] === p
// 只處理最新請(qǐng)求的返回
if (isLatest && !p.expired) {
resolve(res)
} else {
p.expired = true // 避免后面的請(qǐng)求出列后,老的請(qǐng)求繼續(xù)執(zhí)行
}
}).catch(err => {
const isLatest = queue[queue.length - 1] === p
// 只處理最后一次請(qǐng)求的異常
if (isLatest && !p.expired) {
reject(err)
} else {
p.expired = true // 避免后面的請(qǐng)求出列后,老的請(qǐng)求繼續(xù)執(zhí)行
}
}).finally(() => {
const idx = queue.findIndex(item => item === p)
queue.splice(idx, 1)
})
})
queue.push(p)
return p
}
}
總結(jié)
為了解決前端開(kāi)發(fā)中遇到的竟態(tài)請(qǐng)求問(wèn)題,我們提供了兩種解決方案:取消請(qǐng)求 & 忽略請(qǐng)求
這兩種方案都有一定的優(yōu)劣,取消請(qǐng)求會(huì)導(dǎo)致客戶(hù)端主動(dòng)斷開(kāi)連接,可能對(duì)后臺(tái)異常監(jiān)控帶來(lái)影響;忽略請(qǐng)求可能導(dǎo)致前端請(qǐng)求過(guò)于頻繁,增加后臺(tái)服務(wù)器壓力,可以結(jié)合截流/防抖機(jī)制加以?xún)?yōu)化
作者:WeilinerL 鏈接:https://juejin.cn/post/7280740005567332404
往期推薦
技術(shù)交流群
我組建了技術(shù)交流群,里面有很多 大佬,歡迎進(jìn)來(lái)交流、學(xué)習(xí)、共建?;貜?fù) 加群 即可。 后臺(tái)回復(fù)「 電子書(shū) 」即可免費(fèi)獲取 27本 精選的前端電子書(shū)!
“分享、點(diǎn)贊 、 在看” 支持一波??
