js实现并发请求控制

批量请求

先想想如何批量请求?

这里创建一个模拟接口的方法供后续示例使用:

js
function simulateApi(res, delay = 500) {
  return new Promise((resolve, reject) => {
    setTimeout(res ? resolve : reject, delay, res || new Error('Manual error'))
  })
}
  • for...of
js
async function processItemsSequentially() {
  const result = []
  for (const id of [1, 2, 3, 4, 5]) {
    const data = await simulateApi(id)
    result.push(data)
  }
  console.log(result) // [1, 2, 3, 4, 5]
}

processItemsSequentially()

上例是使用for...of实现的顺序请求。

  • Promise.all

使用数组的 map 方法无法返回异步结果,但 Promise.all 可以:

js
let result = []
Promise.all([1, 2, 3, 4, 5].map(id => simulateApi(id)))
  .then(res => result = res)
  .catch(err => console.error(err))

Promise.allSettled 也可以用于批量请求,其异同这里不再赘述。

控制并发请求

某些场景下,我们需要控制页面内并发请求数,而不是一股脑的抛给浏览器

工具库

相关库:

  • p-limit: Run multiple promise-returning & async functions with limited concurrency
  • p-queue: Useful for rate-limiting async (or sync) operations.
  • p-all: Run promise-returning & async functions concurrently with optional limited concurrency
  • p-map: Map over promises concurrently

自行实现

思路:

  • 设置并发上限,记录正在请求的数量
  • 当请求数量未达上限时,直接发起请求,更新计数
  • 有请求完成时,更新计数
  • 当请求数量达到上限时,等待请求完成再发起新的请求

简单来说,就是一个排队等柜台叫号的过程,柜台数也就是并发上限。🤣

以下,仅供参考

源码:

js
export function concurrencyRequest(limit = 5) {
  const tasks = [] // 待请求队列、任务列表
  const queue = [] // 正在请求的队列
  let qId = 0 // 请求id标识
  let qIndex = -1 // 待请求索引

  /**
   * 封装请求函数
   *
   * @param {Function} fn 源异步请求函数
   * @returns {Promise} 返回一个应用并发控制后的promise
   */
  function limitedReqWrap(fn, ...args) {
    const { promise, resolve, reject } = Promise.withResolvers()
    tasks.push({ id: qId++, pending: true, fn, args, resolve, reject })
    checkQueue()
    return promise
  }
  // 叫号:检查请求队列,从任务列表中添加新的请求
  function checkQueue() {
    if (queue.length >= limit) return // 请求队列已满
    if (!tasks[qIndex + 1]) return // 任务列表已空
    // 从任务列表中添加请求
    queue.push(tasks[++qIndex])
    execReqsInQueue()
  }
  // 执行请求队列中未开始的请求
  function execReqsInQueue() {
    for (const req of queue) {
      if (!req.pending) continue
      req.pending = false
      const { id, fn, args, resolve, reject } = req
      fn(...args)
        .then(resolve, reject)
        .finally(() => handleRequestSettled(id))
    }
  }
  // 请求完成。从正在请求队列中移除,尝试添加下一个请求
  function handleRequestSettled(currId) {
    const index = queue.findIndex(p => p.id === currId)
    if (index > -1) {
      queue.splice(index, 1)
      checkQueue()
    }
  }
  // 清空待请求队列(tasks)
  function clearTasks() {
    tasks.length = 0
    qIndex = -1
    // 如果支持,可以在这里中断已发起的异步请求(queue)
  }

  return { limitedReqWrap, clearTasks }
}

/**
 * 模拟接口
 *
 * @param {*} res 返回值
 * @param {number} delay 延迟时间
 */
export function simulateApi(res, delay = 500) {
  return new Promise((resolve, reject) => {
    setTimeout(res ? resolve : reject, delay, res || new Error('Manual error'))
  })
}
vue
<script setup>
import { ref } from 'vue'
import { simulateApi, concurrencyRequest } from './utils'

const requestList = ref([])
let requestId = 0
const { limitedReqWrap, clearTasks } = concurrencyRequest()
// 添加异步请求
function addRequest() {
  const currReq = { status: 'pending', id: requestId++, res: null, err: null }
  const delay = Math.floor(Math.random() * 500 + 100)
  requestList.value.push(currReq)
  // 模拟请求,延时100ms~600ms响应,小概率失败
  limitedReqWrap(simulateApi, delay < 160 ? false : delay, delay)
    .then((val) => {
      const target = requestList.value.find(v => v.id === currReq.id)
      // console.log('success ', currReq.id)
      if (!target) return
      target.status = 'resolved'
      target.res = val
    })
    .catch((err) => {
      const target = requestList.value.find(v => v.id === currReq.id)
      // console.log('failed: ', currReq.id)
      if (!target) return
      target.status = 'rejected'
      target.err = err
    })
}
function add20Requests() {
  for (let i = 0; i < 20; i++) addRequest()
}
function clear() {
  requestList.value = []
  clearTasks()
}
</script>

<template>
  <!-- <h1>测试:并发请求控制</h1> -->
  <button @click="addRequest">点击添加异步请求</button>
  <button @click="add20Requests">点击添加二十个异步请求</button>
  <button @click="clear">清空</button>
  <hr />
  <ol>
    <li
      v-for="item in requestList"
      :key="item.id"
      :style="{ color: { pending: 'orange', resolved: 'green', rejected: 'red' }[item.status] }"
    >
      Status: {{ item.status }}
      <template v-if="item.status !== 'pending'">
        (
        <span v-if="item.res">Result: {{ item.res || '' }}</span>
        <span v-if="item.err">Error: {{ item.err?.message || item.err || '' }}</span>
        )
      </template>
    </li>
  </ol>
</template>

Last updated: