后端接口太慢,前端如何優(yōu)雅地實(shí)現(xiàn)一個(gè)“請(qǐng)求隊(duì)列”,避免并發(fā)打爆服務(wù)器?
有這樣一些場(chǎng)景:
- 頁(yè)面一加載,需要同時(shí)發(fā) 10 個(gè)請(qǐng)求,結(jié)果頁(yè)面卡住,服務(wù)器也快崩了。
- 用戶(hù)可以批量操作,一次點(diǎn)擊觸發(fā)了幾十個(gè)上傳文件的請(qǐng)求,瀏覽器直接轉(zhuǎn)圈圈。
當(dāng)后端處理不過(guò)來(lái)時(shí),前端一股腦地把請(qǐng)求全發(fā)過(guò)去,只會(huì)讓情況更糟。
核心思想就一句話(huà):不要一次性把所有請(qǐng)求都發(fā)出去,讓它們排隊(duì),一個(gè)一個(gè)來(lái),或者一小批一小批來(lái)。
這就好比超市結(jié)賬,只有一個(gè)收銀臺(tái),卻來(lái)了100個(gè)顧客。最好的辦法就是讓他們排隊(duì),而不是一擁而上。我們的“請(qǐng)求隊(duì)列”就是這個(gè)“排隊(duì)管理員”。
直接上代碼:一個(gè)即插即用的請(qǐng)求隊(duì)列
不用復(fù)雜的分析,直接復(fù)制下面的 RequestPool 類(lèi)到我們的項(xiàng)目里。它非常小巧,只有不到 40 行代碼。
/**
* 一個(gè)簡(jiǎn)單的請(qǐng)求池/請(qǐng)求隊(duì)列,用于控制并發(fā)
* @example
* const pool = new RequestPool(3); // 限制并發(fā)數(shù)為 3
* pool.add(() => myFetch('/api/1'));
* pool.add(() => myFetch('/api/2'));
*/
class RequestPool {
/**
* @param {number} limit - 并發(fā)限制數(shù)
*/
constructor(limit = 3) {
this.limit = limit; // 并發(fā)限制數(shù)
this.queue = []; // 等待的請(qǐng)求隊(duì)列
this.running = 0; // 當(dāng)前正在運(yùn)行的請(qǐng)求數(shù)
}
/**
* 添加一個(gè)請(qǐng)求到池中
* @param {Function} requestFn - 一個(gè)返回 Promise 的函數(shù)
* @returns {Promise}
*/
add(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this._run(); // 每次添加后,都嘗試運(yùn)行
});
}
_run() {
// 只有當(dāng) 正在運(yùn)行的請(qǐng)求數(shù) < 限制數(shù) 且 隊(duì)列中有等待的請(qǐng)求時(shí),才執(zhí)行
while (this.running < this.limit && this.queue.length > 0) {
const { requestFn, resolve, reject } = this.queue.shift(); // 取出隊(duì)首的任務(wù)
this.running++;
requestFn()
.then(resolve)
.catch(reject)
.finally(() => {
this.running--; // 請(qǐng)求完成,空出一個(gè)位置
this._run(); // 嘗試運(yùn)行下一個(gè)
});
}
}
}
如何使用?三步搞定!
假設(shè)你有一個(gè)請(qǐng)求函數(shù) mockApi,它會(huì)模擬一個(gè)比較慢的接口。
發(fā)生了什么?
當(dāng)你運(yùn)行上面的代碼,你會(huì)看到:
- [1] 和 [2] 的請(qǐng)求幾乎同時(shí)開(kāi)始。
- [3]、[4]、[5]、[6] 在乖乖排隊(duì)。
- 當(dāng) [1] 或 [2] 中任意一個(gè)完成后,隊(duì)列中的 [3] 馬上就會(huì)開(kāi)始。
- 整個(gè)過(guò)程,同時(shí)運(yùn)行的請(qǐng)求數(shù)永遠(yuǎn)不會(huì)超過(guò) 2 個(gè)。
控制臺(tái)輸出類(lèi)似這樣:
[1] ?? 請(qǐng)求開(kāi)始...
[2] ?? 請(qǐng)求開(kāi)始...
// (此時(shí) 3, 4, 5, 6 在排隊(duì))
[1] ? 請(qǐng)求完成!
[1] 收到結(jié)果: 任務(wù) 1 的結(jié)果
[3] ?? 請(qǐng)求開(kāi)始... // 1號(hào)完成,3號(hào)立刻補(bǔ)上
[2] ? 請(qǐng)求完成!
[2] 收到結(jié)果: 任務(wù) 2 的結(jié)果
[4] ?? 請(qǐng)求開(kāi)始... // 2號(hào)完成,4號(hào)立刻補(bǔ)上
...
它是如何工作的?
(1) add(requestFn): 你扔給它的不是一個(gè)已經(jīng)開(kāi)始的請(qǐng)求,而是一個(gè)“啟動(dòng)器”函數(shù) () => mockApi(i)。它把這個(gè)“啟動(dòng)器”放進(jìn) queue 數(shù)組里排隊(duì)。
(2) _run(): 這是管理員。它會(huì)檢查:
- 現(xiàn)在有空位嗎?(running < limit)
- 有人在排隊(duì)嗎?(queue.length > 0)
- 如果兩個(gè)條件都滿(mǎn)足,就從隊(duì)首叫一個(gè)號(hào)(queue.shift()),讓它開(kāi)始工作(執(zhí)行 requestFn()),并且把正在工作的計(jì)數(shù) running 加一。
(3) .finally(): 這是最關(guān)鍵的一步。每個(gè)請(qǐng)求不管是成功還是失敗,最后都會(huì)執(zhí)行 finally 里的代碼。它會(huì)告訴管理員:“我完事了!”,然后把 running 減一,并再次呼叫管理員 _run() 來(lái)看看能不能讓下一個(gè)人進(jìn)來(lái)。
這樣就形成了一個(gè)完美的自動(dòng)化流程:完成一個(gè),就自動(dòng)啟動(dòng)下一個(gè)。
以后再遇到需要批量發(fā)請(qǐng)求的場(chǎng)景,別再用 Promise.all 一股腦全發(fā)出去了。
把上面那段小小的 RequestPool 代碼復(fù)制到你的項(xiàng)目里,用它來(lái)包裹我們的請(qǐng)求函數(shù)。只需要設(shè)置一個(gè)合理的并發(fā)數(shù)(比如 2 或 3),就能在不修改后端代碼的情況下,大大減輕服務(wù)器的壓力,讓我們的應(yīng)用運(yùn)行得更平穩(wěn)。
這是一種簡(jiǎn)單、優(yōu)雅且非常有效的前端優(yōu)化手段。