事情是這樣,上週四給團隊專欄發了一篇文章,發完以後忘了把帳號切回本身的帳號。而後下午搬磚中場休息的時候刷掘金,在首頁看到一篇關於限制併發請求的文章,以爲有意思就點開看了。我掃了一眼文章中的題目,沒有看文章內容,而後就開始本身解題了。尷尬的是我審錯題了,一開始用團隊帳號在評論裏寫了個 naive 的答案。然而更尷尬的是在我意識到本身審錯題以後,還寫不出正確答案……編程
題目是這樣的:數組
忽略我一開始那個 naive 的答案,在我準備正確地解這個題的時候,想的是寫一個通用方法,限制異步請求能併發執行的次數。那個通用函數寫出來了,長這樣:promise
const limitConcurrency = (fn, max) => {
const pendingTasks = new Set();
return async function(...args) {
while (pendingTasks.size >= max) {
await Promise.race(pendingTasks);
}
const promise = fn.apply(this, args);
const res = promise.catch(() => {});
pendingTasks.add(res);
await res;
pendingTasks.delete(res);
return promise;
};
};
複製代碼
這個 limitConcurrency
函數在閉包裏記錄了當前還沒 resolve 的 promise,而後在 while
循環裏判斷當前進行中的異步操做是否達到了設定的上限;若是達到了就用 Promise.race
等最快的那個異步執行完;當併發的異步操做數量沒有達到上限時,繼續執行當前的異步操做,並將當前的異步操做加到 pendingTasks 裏面,在當前異步操做 resolve 的時候再將其從 pendingTask 裏面刪掉。閉包
而後再回到題目,一開始我思惟比較侷限,想着我必需要等最後一個異步請求執行完再執行回調。怎麼判斷全部請求都執行完了呢?我又用了一個 naive 的方法,當 urls 數組裏面最後一個請求執行完了,我就當全部請求執行完了。我最後這樣寫的:併發
function sendRequest(urls, max, callback) {
const limitFetch = limitConcurrency(fetch, max);
async function go(urlList){
const [head, ...tail] = urlList;
if(tail.length === 0) {
await limitFetch(head);
return callback();
}
limitFetch(head);
go(tail);
}
go(urls);
}
複製代碼
動腦子想想也知道最後一個請求不必定是最後執行完。可是我卡在這裏了,沒辦法了,而後我發沸點求助了,而後各路英雄各顯神通,看到他們的答案我感到懷疑人生,這麼簡單的問題我怎麼就卡殼了?app
精彩答案有不少,這裏我只挑出我以爲最精彩的答案,來自幻☆精靈:異步
function sendRequest(urls, max, callback) {
const len = urls.length;
let idx = 0;
let counter = 0;
function _request() {
// 有請求,有通道
while (idx < len && max > 0) {
max--; // 佔用通道
fetch(urls[idx++]).finally(() => {
max++; // 釋放通道
counter++;
if (counter === len) {
return callback();
} else {
_request();
}
});
}
}
_request();
}
複製代碼
這個答案如此精巧簡潔,我今天上午看到這個答案,而後出去登山,整個途中都在回味這段代碼的精妙…… 它用最少的信息表達了異步和併發的本質。寫出這種代碼,須要的不單單是編程技巧,更多的是對異步和併發的理解。在我完整理解了這段代碼以後,晚上回到家再來看我以前的答案,發現我離正確答案就只差一丟丟了!async
最終完整答案以下:函數
const limitConcurrency = (fn, max) => {
const pendingTasks = new Set();
return async function(...args) {
while (pendingTasks.size >= max) {
await Promise.race(pendingTasks);
}
const promise = fn.apply(this, args);
const res = promise.catch(() => {});
pendingTasks.add(res);
await res;
pendingTasks.delete(res);
return promise;
};
};
async function sendRequest(urls, max, callback) {
const limitFetch = limitConcurrency(fetch, max);
await Promise.all(urls.map(limitFetch));
callback();
}
複製代碼
limitConcurrency
已經保證了個人所有請求的併發上限,我只須要用 Promise.all
來處理每一個併發頻道的最後請求就好了……post
從代碼簡潔性來看,固然是幻☆精靈前輩的解法更優,但我以爲個人解法也不無可取之處。我提供的答案最大的好處是,它抽象出了一個通用函數,這個通用函數能複用。