如何解决前端的“竞态问题”
走过路过发现 bug 请指出,拯救一个辣鸡(但很帅)的少年就靠您啦!!!
什么是竞态问题
竞争问题 race conditions 是并发编程的一种情况,其中两个并发线程或进程竞争资源,最终状态取决于谁先获得资源。
在前端开发中,我们经常这种情况,当发送两个请求时,由于网络原因,如果第一次请求后返回,第二次请求先返回,会导致最后保存的结果是先发送的请求,第二次的请求结果会被覆盖。具体场景比如,我们在输入时查询待选项,又比如我们先点击分页2再点击分页3。
解决思路
1、防抖
我们输入时如果先输入了 “A” 又删除输入了 “B”,如果我们最终展示的是 “A” 相关的待选项,肯定会给用户造成困扰。这是个非常典型的节流应用场景,我们可以等到用户停止输入再发送请求,防止了多次请求的发出也就避免了竞态问题。
2、设置 loading
如果我们在点击分页后进行页面 loading 状态展示,无法再次点击分页,等到请求返回后再允许发送下一次请求,也同样可以避免分页问题。
3、忽略过期请求
如果无法避免竞态问题,我们就需要直面问题并解决——忽略上一次请求的返回结果(当然也可以直接取消请求)。
忽略过期请求,这个是我最开始想到的思路,当我们发送第二个请求时,上一次的请求就应该过期了,我们如果发现请求过期就取消操作。
当然重点是如何判断过期。我们可以通过一个 id 保存最新的请求数,当返回结果后根据 id 判断是否为最新请求。
function mockFetch() {
return new Promise((resolve) => {
setTimeout(resolve, 300)
})
}
// 不断递加 用于记录最新的请求id
let id = 0
// 请求函数
async function request() {
const currentId = (id = id + 1)
console.log('request start', currentId)
await mockFetch()
if (currentId === id) {
console.log('request end', currentId)
} else {
console.log('request expired', currentId, id)
}
}
request()
request()
/*
request start 1
request start 2
request expired 1 2
request end 2
*/
这里的 mockRequest
模拟了异步请求,我们连续发送两次请求,第一次请求返回时,该次请求的 id 已经小于最新 id,这意味着下一个请求已经被发送,当前请求已经过期了。当我们发现请求过期就可以忽略请求结果。
如何让上述逻辑变得通用呢?很自然的想到把当前的全局变量 id 存到闭包中。
function raceFn(fn) {
let id = 0
async function wrapper(...args) {
const currentId = (id = id + 1)
const res = await fn(...args)
if (currentId === id) {
return res
}
// 请求过期就报错
throw new Error('request expired')
}
return wrapper
}
function mockFetch() {
return new Promise((resolve) => {
setTimeout(resolve('success!'), 300)
})
}
const raceMockFetch = raceFn(mockFetch)
async function request() {
try {
const res = await raceMockFetch()
console.log(res);
} catch (e) {
console.log('request expired.', e)
}
}
request() // request expired. Error: request expired
request() // success!
把请求函数进行处理,如果过期了就报错,否则正常返回结果。
转载自:https://juejin.cn/post/7194730444812714040