前端竞态问题粗糙通用处理方案
什么是竞态问题?
前端竞态问题(Race Condition),指的是在异步编程场景中,由于多个异步任务执行顺序的不确定性,导致程序最终结果不符合预期的现象。它就像一场没有 “发令枪” 的赛跑 —— 多个任务同时出发,但到达终点的顺序不可预测,若程序逻辑依赖于特定的顺序,就会引发错误。
示例场景
一个查询列表页,存在多个筛选和排序条件的搜索,在条件A下搜索时接口的响应速度很慢,而在条件B下搜索时接口响应速度很快。
此时,用户先在条件A时搜索,之后用户改变了主意,立马又在改成条件B又搜索了一次。
在接口响应速度相差不大的情况,即使不考虑竞态问题,页面也不会出现什么大问题,因为后执行的搜索的结果后响应,接口的响应和后续处理顺序与用户操作的顺序一致,用户最终看到的搜索结果是正确的(即条件B的搜索结果)。
但如果条件A和条件B对应接口的响应速度差别很大(大于用户两次操作的间隔),就会出现条件B的搜索结果先响应回来,此时页面渲染了条件B的搜索结果,紧接着条件A的响应结果回来了,然后页面就会转而渲染条件A的搜索结果,这就是竞态问题。
时间线图例
前端竞态问题的解决办法
比较精细的解决方案网上已经很多了,我这里就不重复赘述了。
这里我只提供一种情况下的快速通用解决方案:
即在上述列表案例中,实际我们需要展示的就是用户最后一次操作的结果,那么我们每次请求时记录下本次请求的时间戳,同时记录最后一次操作的时间戳,在响应后比较时间戳相等的则为最后一次操作的响应,此时再执行后续渲染即可。
但一个页面我们都要去定义变量记录时间戳属实有点麻烦,那么我们就封装一个公共方法来减少重复代码:
/**
* 异步函数执行包装,传入异步函数后返回一个新的异步函数,
*
* 新的异步函数的返回值会包含一个isLastTime字段,如果新异步函数执行期间又被多次执行时,只有最后一次执行会返回true,否则返回false
*
* 返回值的result字段为传入的异步函数的返回值
*
* 返回值的error字段为传入的异步函数的错误
*
* @param {*} asyncFun 需要执行的异步函数
* @returns
*/
export const useLastTimeTrue = (asyncFun) => {
if (typeof asyncFun !== 'function') {
throw new Error('useLastTimeTrue: asyncFun must be function')
}
let lastTime = 0
// 包装过后的函数
return async function (...args) {
const nowTime = Date.now() + Math.floor(Math.random() * 10000).toString()
lastTime = nowTime
let delayResult
let funError
try {
delayResult = await asyncFun.call(this, ...args)
} catch (error) {
funError = error
}
let result = {
result: delayResult,
isLastTime: false,
error: undefined,
}
if (funError) {
result.error = funError
}
if (nowTime === lastTime) {
result.isLastTime = true
} else {
result.isLastTime = false
}
return result
}
}
使用示例
/** ...其他代码 **/
import { fetchPage } from "@/api/user.js"
import { useLastTimeTrue } from "@/utils/functionUtils.js"
const lttFetchPage = useLastTimeTrue(fetchPage)
const onSearch = () => {
lttFetchPage().then(lttRes => {
if (lttRes.isLastTime) {
// 只处理最后一次请求响应的后续
if (lttRes.error) {
console.error(lttRes.error)
return error
}
const res = lttRes.result
// 后续操作...
}
// 对每次请求都有的部分操作可自行处理
})
}