前端竞态问题粗糙通用处理方案

59 阅读3分钟

前端竞态问题粗糙通用处理方案

什么是竞态问题?

前端竞态问题(Race Condition),指的是在异步编程场景中,由于多个异步任务执行顺序的不确定性,导致程序最终结果不符合预期的现象。它就像一场没有 “发令枪” 的赛跑 —— 多个任务同时出发,但到达终点的顺序不可预测,若程序逻辑依赖于特定的顺序,就会引发错误。

示例场景

一个查询列表页,存在多个筛选和排序条件的搜索,在条件A下搜索时接口的响应速度很慢,而在条件B下搜索时接口响应速度很快。
此时,用户先在条件A时搜索,之后用户改变了主意,立马又在改成条件B又搜索了一次。
在接口响应速度相差不大的情况,即使不考虑竞态问题,页面也不会出现什么大问题,因为后执行的搜索的结果后响应,接口的响应和后续处理顺序与用户操作的顺序一致,用户最终看到的搜索结果是正确的(即条件B的搜索结果)。
但如果条件A和条件B对应接口的响应速度差别很大(大于用户两次操作的间隔),就会出现条件B的搜索结果先响应回来,此时页面渲染了条件B的搜索结果,紧接着条件A的响应结果回来了,然后页面就会转而渲染条件A的搜索结果,这就是竞态问题。

时间线图例

image.png

前端竞态问题的解决办法

比较精细的解决方案网上已经很多了,我这里就不重复赘述了。
这里我只提供一种情况下的快速通用解决方案:
即在上述列表案例中,实际我们需要展示的就是用户最后一次操作的结果,那么我们每次请求时记录下本次请求的时间戳,同时记录最后一次操作的时间戳,在响应后比较时间戳相等的则为最后一次操作的响应,此时再执行后续渲染即可。
但一个页面我们都要去定义变量记录时间戳属实有点麻烦,那么我们就封装一个公共方法来减少重复代码:

/**
 * 异步函数执行包装,传入异步函数后返回一个新的异步函数,
 *
 * 新的异步函数的返回值会包含一个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
          // 后续操作...
      }
      // 对每次请求都有的部分操作可自行处理
    })
}