使用 Promise.withResolvers 实现合并连续请求

使用 Promise.withResolvers() 合并连续请求。

方案一:返回最早的未落定期约

策略:存在未落定的请求时,后续请求返回该期约。该策略思路简明,实现也非常简单。

js
/**
 * 合并相同请求(连续调用包装后的请求方法,前一次调用未完成时,后一次调用返回前一次调用的结果)
 *
 * @param {Function} request
 * @returns {Object} { mergedAsyncRequest: 合并后的异步请求方法 }
 * @example
 * import useMergeContinuousAsync from '@/use/merge-continuous-async'
 *
 * const originalRequestFunction = (...args) => axios.get(...args)
 * const { mergedAsyncRequest } = useMergeContinuousAsync(originalRequestFunction)
 * mergedAsyncRequest(111)
 * setTimeout(mergedAsyncRequest, 200, 222)
 */
export default request => {
  if (!(request instanceof Function)) throw new Error('request must be a function')
  let promise, resolve, reject

  function mergedAsyncRequest(...args) {
    if (promise) return promise
    ;({ promise, resolve, reject } = Promise.withResolvers())
    request(...args)
      .then(resolve, reject)
      .finally(() => {
        promise = undefined
      })
    return promise
  }

  return { mergedAsyncRequest }
}
vue
<script setup>
import { ref } from 'vue'
import useMergeContinuousAsync from './merge-continuous-async'

const reqs = ref([])

const { mergedAsyncRequest } = useMergeContinuousAsync(testApi)

async function addAsyncReq() {
  const timestamp = +new Date()
  reqs.value.push({
    timestamp,
    loaded: false,
    resFlag: '',
  })
  let currReq
  try {
    const res = await mergedAsyncRequest(timestamp)
    currReq = reqs.value.find(r => r.timestamp === timestamp)
    currReq.resFlag = res.data[0]
  } catch (err) {}
  if (currReq) currReq.loaded = true
}

function testApi(...args) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 1000, { data: args })
  })
}
</script>

<template>
  <h1>Merge continuous async</h1>
  <button @click="addAsyncReq">Add async request</button>
  <hr />
  <label>Async request list:</label>
  <div v-for="item in reqs" :key="item.timestamp">
    <label>Flag: {{ item.timestamp }}, </label>
    <span>{{ item.loaded ? 'loaded' : 'loading' }}; </span>
    <span>resFlag:{{ item.resFlag }}</span>
  </div>
</template>

演练场调试

方案二:取消旧的请求

参考连续接口请求,处理方案可以是取消所有旧的请求,仅保留最新的请求。

js
export default request => {
  if (!(request instanceof Function)) throw new Error('request must be a function')
  let promise, resolve, reject

  function mergedAsyncRequest(...args) {
    if (promise) {
      reject(new Error('Cancalled'))
    }
    ;({ promise, resolve, reject } = Promise.withResolvers())
    request(...args).then(resolve, reject)
    return promise
  }

  return { mergedAsyncRequest }
}

第 7 行存在一个无效取消的问题,但拒绝一个已落定的期约不会生效也不会报错

演练场调试

方案三:返回最后的未落定期约

如果希望实现的是合并,但以最新请求的落定为准,情况可能会有点复杂,需要将未落定的期约保持与最新的期约一致。

Last updated: