【异步处理有序性】前端多次发起同一个请求或调用同一个异步方法,如何确保异步结果处理顺序和发起顺序一致。

506 阅读2分钟

示例场景

在前端场景中,经常会出现发起多次请求的场景,由于每次请求花费的时间都不一致,可能会出现数据错乱/新请求结果被旧请求结果覆盖的情况,如:

  1. 通过搜索框进行检索
    • 如检索"123",可能参数为"1"/"123"时,会分别发出一次请求,当参数"1"的请求时间过长,就可能会导致我们将参数为"1"的请求结果作为参数为"123"的请求结果进行展示。
  2. 列表加载更多
    • 首屏幕内容多的情况下可能需要同时请求page1/page2/page3的数据,需要多次触发加载更多将多页数据进行拼接展示,如请求结果处理顺序与发起顺序不一致,会导致内容排序出现问题。
  3. 异步处理url上的参数并发起请求
    • 由于初始化时可能会基于默认参数先发起一次请求-request1,然后异步处理(如在 react 中的 useEffect 中处理)url上的参数再次发起请求-request2,此时如果 request1 耗费的时间要比 requst2 多,就会导致 request2 的请求结果被 request1 的请求结果覆盖,与预期结果不符.

示例代码

const fetchFileList = async (params: IFileListParam) => {
    console.log('input>>>', params.page)
    const res = await queryFileList(params)
    console.log('output<<<', params.page)
    setData(origin=>origin.concat(res.data))
}

[1,2,3].forEach(page=>{ fetchFileList({page})})

以上代码,为了保证列表数据排序是正确的,setData这段代码需要有序执行,然而我们得到的日志是这样的:

image.png

可以看出请求完成后,先处理的page3的数据,再处理的page2的数据,这样便会导致页面上的数据展示排序出现问题,其原因是page3的请求比page2的请求先完成

image.png

解决方案

为此,我们可以封装一个AsyncSortedRequest,确保请求有序,不会因为请求耗费时间不同导致数据错乱.

interface IQueue {
  status: 'pending' | 'finished'
  resolve?: (val: any) => void
  reject?: (val: any) => void
  value?: any
}

/**
 * 确保请求有序,不会因为请求耗费时间不同导致数据错乱
 * @param fn - 请求函数
 * @returns 
 */
export function AsyncSortedRequest(fn: Function) {
  // 创建队列以保存所有的请求
  const queue: IQueue[] = []

  // 检查队列状态并执行
  const checkQueue = () => {
    if (!queue.map(item => item.status).some(status => status === 'pending')) {
      while (!!queue.length) {
        const request = queue.shift()
        request?.resolve && request.resolve(request.value)
        request?.reject && request.reject(request.value)
      }
    }
  }
  
  return function (...args) {
    // 请求占位
    const index = queue.length
    queue[index] = {
      status: 'pending'
    }
    
    return new Promise((resolve, reject) => {
      // 处理请求结果并保存
      fn(...args).then(res => {
        queue[index] = {
          value: res,
          status: 'finished',
          resolve
        }
      }, err => {
        queue[index] = {
          value: err,
          status: 'finished',
          reject
        }
      }).finally(() => {
        // 检查队列
        checkQueue()
      })
    })
  }
}

AsyncSortedRequest 有了,之后我们将queryFileList作为参数传入即可:

const SortedRequest = AsyncSortedRequest(queryFileList)

const fetchFileList = async (params: IFileListParam) => {
    console.log('input>>>', params.page)
    const res = await SortedRequest(params)
    console.log('output<<<', params.page)
    setData(origin=>origin.concat(res.data))
}

[1,2,3].forEach(page=>{ fetchFileList({page})})

得到正确的有序日志输出是: image.png