23行程式碼實現一個帶併發數限制的fetch請求函式

zenghongtu發表於2019-03-14

早上吃早餐邊逛掘金的時候看到一個面試題,看下面的各位大佬各顯神通,我也手癢,拿起我的機械鍵盤一頓亂敲,發現只需要30行程式碼就可以實現,似不似很膩害??,快來跟我往下翻?

2019-03-14晚補充:吃了晚飯看到大佬 @serialcoder 的回覆,直接石化了,又仔細讀了一遍題目,發現確實審錯題目了,太大意了?我之前的實現是多個請求同時進行,但不是併發,而是多個同時進行的序列。於是乎我開始了又一頓亂敲,差不多半小時搞定。這會應該沒錯了,而且只用了23行,趕緊把標題也給改了?

原題

23行程式碼實現一個帶併發數限制的fetch請求函式

題目來源:記一道控制併發數的前端面試題【手動維護 HTTP 請求排隊】

思路

讀題

快速過一遍題目,可以獲得這幾個主要資訊:(罪魁禍首,讀題要仔仔細細的讀,不能貪快?)

  1. 批量請求
  2. 可控制併發度
  3. 全部請求結束,執行 callback

解題

  1. 批量請求

要實現批量請求,而且並不需要按順序發起請求(如果需要按順序可以存入佇列中,按優先順序則可以存入優先佇列中),所以這裡我們存入陣列中即可,然後進行遍歷,取出數字中的每一項丟去fetch中進行呼叫。

  1. 可控制併發度

控制併發數,一個簡單的辦法就是對陣列進行切片,分成一段一段,完成一段後再呼叫另一段。這裡我們可以使用遞迴或者迴圈來實現,我覺得遞迴比較直觀,所以這裡使用遞迴來實現。

本題的難點就在這裡,之前做錯了就是這一步。在控制併發數的同時,每結束一個請求併發起一個新的請求。依舊使用遞迴的方式,但這次新增一個請求佇列,然後我們只要維護這個佇列,每次發起一個請求就新增進去,結束一個就丟出來,繼而實現了控制併發。

  1. 全部請求結束,執行 callback

因為是非同步請求,我們無法寄希望於安裝正常的順序在函式呼叫後執行,但是每次fetch有返回結果會呼叫then或者catch,我們可以在這個時候判斷請求陣列是否為空就可以知道是否全部被呼叫完

寫題

之前❌的程式碼

(以下可以不看,是之前寫的非正確解題)

這一步就沒什麼可以說的了,擼起袖子就是敲~

function handleFetchQueue(urls, max, callback) {
  const requestArr = [];
  urls.forEach((item, idx) => {
    const i = Math.floor(idx / max);
    if (requestArr[i]) {
      requestArr[i].push(item)
    } else {
      requestArr[i] = [item]
    }
  });

  const handleSubRequests = (subReqs) => {
    const results = [];
    subReqs.forEach(req => {
      fetch(req).then(res => {
        if (results.push(res) === max) {
          if (requestArr.length < 1) {
            'function' === typeof callback && callback(results)
          } else {
            handleSubRequests(requestArr.shift(), requestArr, max)
          }
        }
      }).catch(e => {
        results.push(e)
      })
    })
  };
  handleSubRequests(requestArr.shift())
}
複製程式碼

這裡需要稍微提一下的兩個小技巧:

  • 通過Math.floor(idx / max)我們可以輕鬆的將每一項推入正確的子陣列中
  • 善用陣列的返回值:results.push(res)返回陣列長度,直接用就好啦

附上完整測試程式碼:

function handleFetchQueue(urls, max, callback) {
  const requestArr = [];
  urls.forEach((item, idx) => {
    const i = Math.floor(idx / max);
    if (requestArr[i]) {
      requestArr[i].push(item)
    } else {
      requestArr[i] = [item]
    }
  });

  const handleSubRequests = (subReqs) => {
    const results = [];
    subReqs.forEach(req => {
      fetch(req).then(res => {
        if (results.push(res) === max) {
          if (requestArr.length < 1) {
            'function' === typeof callback && callback(results)
          } else {
            handleSubRequests(requestArr.shift(), requestArr, max)
          }
        }
      }).catch(e => {
        results.push(e)
      })
    })
  };
  handleSubRequests(requestArr.shift())
}


const urls = Array.from({length: 10}, (v, k) => k);

const fetch = function (idx) {
  return new Promise(resolve => {
    console.log(`start request ${idx}`);
    // 模擬請求時間
    const timeout = parseInt(Math.random() * 1e4);
    setTimeout(() => {
      console.log(`end request ${idx}`);
      resolve(idx)
    }, timeout)
  })
};

const max = 4;

const callback = () => {
  console.log('run callback');
};

handleFetchQueue(urls, max, callback);
複製程式碼

因為我在 Node 中執行,(lan)懶(ai)得(fa)丟(zuo)瀏覽器中去跑了,所以隨手模擬了一個fetch函式。

✅的程式碼

function handleFetchQueue(urls, max, callback) {
  const urlCount = urls.length;
  const requestsQueue = [];
  const results = [];
  let i = 0;
  const handleRequest = (url) => {
    const req = fetch(url).then(res => {
      const len = results.push(res);
      if (len < urlCount && i + 1 < urlCount) {
        requestsQueue.shift();
        handleRequest(urls[++i])
      } else if (len === urlCount) {
        'function' === typeof callback && callback(results)
      }
    }).catch(e => {
      results.push(e)
    });
    if (requestsQueue.push(req) < max) {
      handleRequest(urls[++i])
    }
  };
  handleRequest(urls[i])
}
複製程式碼

❌程式碼和✅程式碼測試比對

先來看錯誤的,可以發現是序列執行的:

23行程式碼實現一個帶併發數限制的fetch請求函式

再看正確的程式碼,這才是併發:

23行程式碼實現一個帶併發數限制的fetch請求函式

貼上完整程式碼:

function handleFetchQueue(urls, max, callback) {
  const urlCount = urls.length;
  const requestsQueue = [];
  const results = [];
  let i = 0;
  const handleRequest = (url) => {
    const req = fetch(url).then(res => {
      console.log('當前併發: '+requestsQueue);
      const len = results.push(res);
      if (len < urlCount && i + 1 < urlCount) {
        requestsQueue.shift();
        handleRequest(urls[++i])
      } else if (len === urlCount) {
        'function' === typeof callback && callback(results)
      }
    }).catch(e => {
      results.push(e)
    });
    if (requestsQueue.push(req) < max) {
      handleRequest(urls[++i])
    }
  };
  handleRequest(urls[i])
}


const urls = Array.from({length: 10}, (v, k) => k);

const fetch = function (idx) {
  return new Promise(resolve => {
    console.log(`start request ${idx}`);
    const timeout = parseInt(Math.random() * 1e4);
    setTimeout(() => {
      console.log(`end request ${idx}`);
      resolve(idx)
    }, timeout)
  })
};

const max = 4;

const callback = () => {
  console.log('run callback');
};


handleFetchQueue(urls, max, callback);
複製程式碼

總結

通過讀題解題寫題三個步驟(自創的?),可以讓我們的思路非常清晰,非常 easy 的解決面試題。

題目敲了20分鐘程式碼解決,但是文章寫了一小時,太不容易了,覺得不錯請給我鼓個掌~~

相關文章