短小且優雅的Promise併發控制實現

語言: CN / TW / HK

前言

Promise是前端工程師寫程式碼最常用的知識點,也是大廠面試最愛考察的點。在做大廠校招面試整理的時候做個初略多統計,700份面經裡有234份面經出現Promise。而手寫程式碼實現Promise的併發控制算是其中出現頻繁且稍微難度高一點的題目。

實現

這裡給出一個參考實現,出自這裡,程式碼只有十幾行,但實現的非常巧妙。

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = [];               //2
  const executing = [];         //3
  for (const item of array) {   //4
    const p = Promise.resolve().then(() => iteratorFn(item));  //5
    ret.push(p);                //6
    if (poolLimit <= array.length) { //7
      const e = p.then(() => executing.splice(executing.indexOf(e), 1)); //8
      executing.push(e);        //9
      if (executing.length >= poolLimit) {  //10
        await Promise.race(executing);      //11
      }
    }
  }
  return Promise.all(ret);     //15
}

程式碼雖然不多,但需要對Promise非常熟悉才能理解,下面就模擬程式碼執行跑一遍執行過程。

假設 poolLimit = 3, array是一個長度為10的url列表, iteratorFn是一個返回Promose物件的函式用於傳送請求,模擬一下執行過程:

  1. line2:建立陣列ret,用於存放全部的Promise物件
  2. line3:建立陣列execting,用於存放併發限制的處於Pending狀態的Promise物件
  3. line4:item是array的第一項
  4. line5: iteratorFn(item) 得到一個pending狀態的Promise物件 p。(這裡之所以不直接 p = iteratorFn(item),是為了相容iteratorFn是同步函式的場景,保證返回的p一定是Promose物件,見下方測試程式碼)
  5. line6:p放入ret
  6. line7:如果限制數量poolLimit 小於等於 陣列的總長度再執行限制。當前poolLimit=3,arr.length=10,進入if邏輯
  7. line8 :根據剛剛的p建立一個Promise物件e,等p resolve的時候才執行then裡的回撥,把e從executing陣列移除(PS:目前e還沒放入陣列,在line9會放進去)
  8. line9:把e放入executing
  9. line10:目前executing長度小於poolLimit限制長度3,不進入if,回到line4執行下一次迴圈
  10. .... 迴圈執行到第3次時,到達line10,此時ret陣列為[p1_ pending, p2_pending , p3_ pending],executing陣列為[e1_pending, e2_pending, e3_pending],其中p1_pending 的resolve會觸發e1的移出和resolve(第8行then裡的箭頭函式執行完e就resovle)
  11. line11:卡住,等待executing 數組裡的[e1,e2,e3]看哪個最快resolve 。假設p2最先resolve,p2的resolve觸發e2的resolve,當e2 resolve 之後,Promise.race(executing)得到結果, 此刻回到line4,for迴圈才進入下一輪,execting數組裡為[e1_ pending, e3_pending ],ret陣列為[p1 pending, p2_fulfilled, p3_pending ]。
  12. line4:... ,繼續下一輪迴圈,execting陣列始終保持最多不超過3個
  13. ...
  14. line15:當for迴圈結束之後,ret數組裡包含全部arr封裝的promise物件,返回Promise.all(ret),得到新的Promise物件(等ret所有的全resolve後該Promise物件才resolve,同時得到所有資料)

測試

以下是測試程式碼:

const curl = (i) => {
  console.log('開始' + i);
  return new Promise((resolve) => setTimeout(() => {
    resolve(i);
    console.log('結束' + i);
  }, 1000+Math.random()*1000));
};

/*
const curl = (i) => { 
  console.log('開始' + i);
  return i;
};
*/
let urls = Array(10).fill(0).map((v,i) => i);
(async () => {
    const res = await asyncPool(3, urls, curl);
    console.log(res);
 })();

飢人谷試學營還在進行中,系統班年前最後一波,儘早上車敢2022春招末尾或者7月份提前批。