事情是這樣,上週四給團隊專欄發了一篇文章,發完之後忘了把賬號切回自己的賬號。然後下午搬磚中場休息的時候刷掘金,在首頁看到一篇關於限制併發請求的文章,覺得有意思就點開看了。我掃了一眼文章中的題目,沒有看文章內容,然後就開始自己解題了。尷尬的是我審錯題了,一開始用團隊賬號在評論裡寫了個 naive 的答案。然而更尷尬的是在我意識到自己審錯題之後,還寫不出正確答案……
題目是這樣的:
忽略我一開始那個 naive 的答案,在我準備正確地解這個題的時候,想的是寫一個通用方法,限制非同步請求能併發執行的次數。那個通用函式寫出來了,長這樣:
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);
}
複製程式碼
動腦子想一想也知道最後一個請求不一定是最後執行完。但是我卡在這裡了,沒辦法了,然後我發沸點求助了,然後各路英雄各顯神通,看到他們的答案我感到懷疑人生,這麼簡單的問題我怎麼就卡殼了?
精彩答案有很多,這裡我只挑出我覺得最精彩的答案,來自幻☆精靈:
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();
}
複製程式碼
這個答案如此精巧簡潔,我今天上午看到這個答案,然後出去爬山,整個途中都在回味這段程式碼的精妙…… 它用最少的資訊表達了非同步和併發的本質。寫出這種程式碼,需要的不僅僅是程式設計技巧,更多的是對非同步和併發的理解。在我完整理解了這段程式碼之後,晚上回到家再來看我之前的答案,發現我離正確答案就只差一丟丟了!
最終完整答案如下:
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
來處理每個併發頻道的最後請求就行了……
從程式碼簡潔性來看,當然是幻☆精靈前輩的解法更優,但我覺得我的解法也不無可取之處。我提供的答案最大的好處是,它抽象出了一個通用函式,這個通用函式能複用。
【廣告】
螞蟻保險體驗技術團隊招聘前端。我們來自螞蟻金服保險事業群(杭州/上海)。我們是一個年輕的團隊(沒有歷史技術棧包袱),目前平均年齡92年(去除一個最高分8x年-團隊leader,去除一個最低分97年-實習小老弟)。我們支援了阿里集團幾乎所有的保險業務。18年我們產出的相互寶轟動保險界,19年我們更有多個重量級專案籌備動員中。現伴隨著事業群的高速發展,團隊也在迅速擴張,歡迎各位前端高手加入我們~ 我們希望你是:技術上基礎紮實、某領域深入(Node/互動營銷/資料視覺化等);學習上善於沉澱、持續學習;性格上樂觀開朗、活潑外向。 如有興趣加入我們,歡迎傳送簡歷至郵箱:ray.hl@alipay.com