前端在發送請求之後,在等待請求返回的時候處於空閒狀態,僅對這個請求來講,不需要處理任何事情。將處理請求結果的函數放到微任務隊列裏面,等請求返回之後再進行處理,就可以將這段時間釋放,去做其他事情。使用這種方式發送多個請求,就可以實現併發的效果。
如果一個頁面內的請求數量過多,請求的規模變大,就需要建立一個管理請求的隊列,統一管理請求的發送和處理。目前主流的處理方案類似下面的代碼:
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,提出建議。