記一次尷尬的評論翻車事件

serialcoder發表於2019-03-17

事情是這樣,上週四給團隊專欄發了一篇文章,發完之後忘了把賬號切回自己的賬號。然後下午搬磚中場休息的時候刷掘金,在首頁看到一篇關於限制併發請求的文章,覺得有意思就點開看了。我掃了一眼文章中的題目,沒有看文章內容,然後就開始自己解題了。尷尬的是我審錯題了,一開始用團隊賬號在評論裡寫了個 naive 的答案。然而更尷尬的是在我意識到自己審錯題之後,還寫不出正確答案……

題目是這樣的:

req

忽略我一開始那個 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

相關文章