如何处理前端开发中的竞态请求
点击上方 前端阳光 ,关注公众号
回复 加群 ,加入技术交流群交流群
前言
竞态条件(Race Conditions)在前端开发中是一种常见的问题,特别是在多个异步操作同时竞争资源或执行时。这可能导致意外的结果,如数据不一致、重复请求和UI错误。
举个🌰
一个最常见的场景就是选项卡切换,用户快速切换Tab,由于网络延迟等原因,很可能最后停留的Tab所展示的内容,并不是用户想要看到的内容
出现这种情况是因为,用户最先点击的Tab请求把最后点击的Tab请求覆盖了(由于网络原因,最先发出的请求最后到达):
解决方案
取消请求
通常我们可以在新的请求发起之前,将旧的、未到达的请求给取消掉,这样旧的请求就不会覆盖新的请求了
XMLHttpRequest 取消请求
XMLHttpRequest(XHR)是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。
如果请求已被发出,可以使用 abort() 方法立刻中止请求。
const xhr= new XMLHttpRequest();
xhr.open('GET', 'https://xxx');
xhr.send();
// 取消请求
xhr.abort();
Fetch API 取消请求
fetch 号称是 AJAX 的替代品,出现于 ES6,它也可以发出类似 XMLHttpRequest 的网络请求。
主要的区别在于 fetch 使用了 Promise,要中止 fetch 发出的请求,需要使用 AbortController。
const controller = new AbortController();
const signal = controller.signal;
fetch('/xxx', {
signal,
}).then(function(response) {
//...
});
// 取消请求
controller.abort();
Axios cancel Token 取消请求
相比原生 API,大多项目都会选择 axios 进行请求。
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请求会走到异常处理,我们需要对这种错误进行过滤
if (Axios.isCancel(err)) {
return {} as GetSomeResourceRsp
}
throw err
})
}
忽略请求
相较于取消请求,忽略请求更为通用。我们只需要关注我们最后一次请求的结果,如果某次请求的返回结果并不是最新的,那么我们就忽略掉这个请求。忽略请求的一个精髓在于终止Promise的响应,这也是字节面试官经常问到的一个问题,我们需要做的就是返回一个Pending的Promise,从而做到终止的效果 使用锁标记
这里的锁只的是某个唯一标识,通过这个标识来对比当前请求是否过期,如下文的prevTimestamp
我们通过闭包,实现对prevTimestamp的缓存,从而做到对每次请求的返回进行对比的效果
/**
* 处理竞态请求
*
* @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 => {
// 只处理最新请求的返回
if (curTimestamp === prevTimestamp) {
resolve(res)
}
}).catch(err => {
// 只处理最后一次请求的异常
if (curTimestamp === prevTimestamp) {
reject(err)
}
})
})
}
}
在使用的时候,我们就可以用这个方法把真正要发出的请求包一下(针对某一个具体的原子请求,从而做到多请求皆可竟态处理)
import Axios from 'axios'
const _getSomeResource = (params: GetSomeResourceReq) => {
return Axios.post('/xxx', params)
}
export const getSomeResource = useFetch(_getSomeResource)
使用队列
与使用锁类似,我们把每个请求都放进队列里,对每次返回的请求进行判断,如果这个请求不是最新的请求,那么就忽略掉
/**
* 处理竞态请求
*
* @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
// 只处理最新请求的返回
if (isLatest && !p.expired) {
resolve(res)
} else {
p.expired = true // 避免后面的请求出列后,老的请求继续执行
}
}).catch(err => {
const isLatest = queue[queue.length - 1] === p
// 只处理最后一次请求的异常
if (isLatest && !p.expired) {
reject(err)
} else {
p.expired = true // 避免后面的请求出列后,老的请求继续执行
}
}).finally(() => {
const idx = queue.findIndex(item => item === p)
queue.splice(idx, 1)
})
})
queue.push(p)
return p
}
}
总结
为了解决前端开发中遇到的竟态请求问题,我们提供了两种解决方案:取消请求 & 忽略请求
这两种方案都有一定的优劣,取消请求会导致客户端主动断开连接,可能对后台异常监控带来影响;忽略请求可能导致前端请求过于频繁,增加后台服务器压力,可以结合截流/防抖机制加以优化
作者:WeilinerL 链接:https://juejin.cn/post/7280740005567332404
往期推荐
技术交流群
我组建了技术交流群,里面有很多 大佬,欢迎进来交流、学习、共建。回复 加群 即可。 后台回复「 电子书 」即可免费获取 27本 精选的前端电子书!
“分享、点赞 、 在看” 支持一波👍