前端在發送請求之後,在等待請求返回的時候處於空閒狀態,僅對這個請求來講,不需要處理任何事情。將處理請求結果的函數放到微任務隊列裏面,等請求返回之後再進行處理,就可以將這段時間釋放,去做其他事情。使用這種方式發送多個請求,就可以實現併發的效果。

如果一個頁面內的請求數量過多,請求的規模變大,就需要建立一個管理請求的隊列,統一管理請求的發送和處理。目前主流的處理方案類似下面的代碼:

function fetchQueue(urls: Promise<any>[], maxNum: number) {
  return new Promise((resolve, reject) => {
    if (urls.length === 0) {
      resolve([])
      return
    }

    const result = new Array(urls.length)
    let index = 0
    const request = async () => {
      const i = index
      const url = urls[index]
      index++
      try {
        const data = await url
        result[i] = data
      } catch (e) {
        result[i] = e
      } finally {
        if (index < urls.length) {
          request()
        } else {
          console.log(result)
          resolve(result)
        }
      }
    }

    for (let i = 0; i < Math.min(maxNum, urls.length); i++) {
      request()
    }
  })
}

上面的代碼傳入一個 Promise 數組 urls,在 request 函數中通過索引 index 取出隊列裏的 promise,然後自增 index,以 await 的方式等待取出的 promise 執行完成,最後在 finally 內部再次調用 request 函數,進行鏈式調用,不斷的從隊列中取出 promise,然後執行。

通過在 promise 的 finally 裏面調用函數自身也可以達到這種效果,這裏將其稱為一個請求流程,該請求流程同步執行。fetchQueue 中通過 for 循環開啓了多個這樣的請求流程,可以達到併發的效果,並且控制最大請求數,避免過高的內存和性能佔用。

如果隊列裏的請求沒有得到補充,在將隊列裏的請求消耗完成之後,通過 for 循環開啓的這些流程就會停止執行,不會再次開啓。這時候可以為每一個流程分配一個狀態,通過輪詢這些流程的狀態,在流程結束之後再次開啓,這樣就能應對需要發送大量連續請求的場景。偽代碼如下:

const concurrency = 6
const state = new Array(concurrency).fill(false)
const urls = []
const request = (i: number) => {
    if (urls.length > 0) {
        state[i] = true
        ...
        finally(() => {
            request(i)
        })
        ...
    } else {
        state[i] = false
    }
}

setInterval(() => {
    for (let i = 0; i < concurrency; i++) {
        if (!state[i]) {
            request(i)
        }
    }
}, 500)

上面的代碼使用 state 存儲各流程的狀態,初始為 false,表示流程沒有開啓。通過定時器每 500 毫秒檢查一次各流程的狀態,檢測到流程關閉或者未開啓之後嘗試開啓此流程,再然後通過隊列裏是否存在元素設置 state 的狀態。

為了找到最合適的併發數,可以結合平均請求時間、每秒鐘入隊的請求數來計算,通過平均請求時間來計算出每秒鐘可以完成請求的平均數量,將每秒入隊的請求數除以每秒能完成請求的平均數量,就可以得到最合適大小的併發數。

mlfetch 是本人針對需要發送大量連續請求的場景開發的請求隊列管理庫,包括了上面介紹的所有功能點,項目還在完善階段,歡迎在 github 上開個 issue,提出建議。