Promise 竟被他玩出了四十八種花樣

最近,阿寶哥在梳理 CLI(Command Line Interface)的相關內容,就對優秀的 Lerna 產生了興趣,於是開始 “啃” 起了它的源碼。在閱讀開源項目時,阿寶哥習慣先閱讀項目的 「README.md」 文檔和 「package.json」 文件,而在 「package.json」 文件的 「dependencies」 字段中,阿寶哥見到了多個 「p-」* 的依賴包:

{
  "name""lerna-monorepo",
  "version""4.0.0-monorepo", 
  "dependencies"{
    "p-map""^4.0.0",
    "p-map-series""^2.1.0",
    "p-pipe""^3.1.0",
    "p-queue""^6.6.2",
    "p-reduce""^2.1.0",
    "p-waterfall""^2.1.1"
  }
}

提示:如果你想知道阿寶哥如何閱讀開源項目的話,可以閱讀 使用這些思路與技巧,我讀懂了多個優秀的開源項目 這篇文章。

之後,阿寶哥順藤摸瓜找到了 promise-fun (3.5K) 這個項目。該項目的作者 「sindresorhus」 是一個全職做開源的大牛,Github 上擁有 「43.9K」 的關注者。同時,他還維護了多個優秀的開源項目,比如 awesome (167K)、awesome-nodejs (42K)、got (9.8K)、ora (7.1K) 和 screenfull.js (6.1K) 等項目。

(圖片來源 —— https://github.com/sindresorhus)

promise-fun 項目收錄了 「sindresorhus」 寫過的 「48」 個與 Promise 相關的模塊,比如前面見到的 「p-map」「p-map-series」「p-pipe」「p-waterfall」 等模塊。本文阿寶哥將篩選一些比較常用的模塊,來詳細分析每個模塊的用法和具體的代碼實現。

這些模塊提供了很多有用的方法,利用這些方法,可以幫我們解決工作中一些很常見的問題,比如實現併發控制、異步任務處理等,特別是處理多種控制流,比如 「series」「waterfall」「all」「race」「forever」 等。

掘友們,準備好了沒?讓我們一起開啓 promise-fun 項目的 “探祕” 之旅。

初始化示例項目

創建一個新的 「learn-promise-fun」 項目,然後在該項目的根目錄下,執行 npm init -y 命令進行項目初始化操作。當該命令成功運行後,在項目根目錄下將會生成 「package.json」 文件。由於 promise-fun 項目中的很多模塊使用了 ES Module,爲了保證後續的示例代碼能夠正常運行,我們需要在 「package.json」 文件中,增加 「type」 字段並設置它的值爲 「"module"」

由於阿寶哥本地 Node.js 的版本是 「v12.16.2」,要運行 ES Module 模塊,還要添加 「--experimental-modules」 命令行選項。而如果你不想看到警告消息,還可以添加 「--no-warnings」 命令行選項。此外,爲了避免每次運行示例代碼時,都需要輸入以上命令行選項,我們可以在 「package.json」「scripts」 字段中定義相應的 「npm script」 命令,具體如下所示:

{
  "name""learn-promise-fun",
  "type""module",
  "scripts"{
    "pall""node --experimental-modules ./p-all/p-all.test.js",
    "pfilter""node --experimental-modules ./p-filter/p-filter.test.js",
    "pforever""node --experimental-modules ./p-forever/p-forever.test.js",
    "preduce""node --experimental-modules ./p-reduce/p-reduce.test.js",
    ...
  },
}

在完成項目初始化之後,我們先來回顧一下大家平時用得比較多的 reducemapfilter 數組方法的特點:

提示:上圖通過👉  「https://carbon.now.sh/」 在線網頁製作生成。

相信大家對圖中的 「Array.prototype.reduce」 方法不會陌生,該方法用於對數組中的每個元素執行一個 「reducer」 函數,並將結果彙總爲單個返回值。對應的使用示例,如下所示:

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer)); // 10

其中 「reducer」 函數接收 4 個參數:

而接下來,我們要介紹的 p-reduce 模塊,就提供了跟 「Array.prototype.reduce」 方法類似的功能。

p-reduce

Reduce a list of values using promises into a promise for a value

https://github.com/sindresorhus/p-reduce

使用說明

p-reduce 適用於需要根據異步資源計算累加值的場景。該模塊默認導出了一個 「pReduce」 函數,該函數的簽名如下:

「pReduce(input, reducer, initialValue): Promise」

瞭解完 「pReduce」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-reduce/p-reduce.test.js
import delay from "delay";
import pReduce from "p-reduce";

const inputs = [Promise.resolve(1), delay(50, { value: 6 }), 8];

async function main() {
  const result = await pReduce(inputs, async (a, b) => a + b, 0);
  console.dir(result); // 輸出結果:15
}

main();

在以上示例中,我們導入了 「delay」 模塊默認導出的 delay 方法,該方法可用於按照給定的時間,延遲一個 Promise 對象。即在給定的時間之後,Promise 狀態纔會變成 「resolved」。默認情況下,「delay」 模塊內部是通過 setTimeout API 來實現延遲功能的。示例中 delay(50, { value: 6 }) 表示延遲 50ms 後,Promise 對象的返回值爲 「6」。而在 main 函數內部,我們使用了 pReduce 函數來計算 inputs 數組元素的累加值。當以上代碼成功運行之後,命令行會輸出 「15」

下面我們來分析一下 pReduce 函數內部是如何實現的。

源碼分析

// https://github.com/sindresorhus/p-reduce/blob/main/index.js
export default async function pReduce(iterable, reducer, initialValue) {
  return new Promise((resolve, reject) ={
    const iterator = iterable[Symbol.iterator](); // 獲取迭代器
    let index = 0; // 索引值

    const next = async (total) ={
      const element = iterator.next(); // 獲取下一項

      if (element.done) { // 判斷迭代器是否迭代完成
        resolve(total);
        return;
      }

      try {
        const [resolvedTotal, resolvedValue] = await Promise.all([
          total,
          element.value,
        ]);
        // 迭代下一項
        // reducer(previousValue, currentValue, index): Function
        next(reducer(resolvedTotal, resolvedValue, index++));
      } catch (error) {
        reject(error);
      }
    };

    // 使用初始值,開始迭代
    next(initialValue);
  });
}

在以上代碼中,核心的流程就是通過獲取 iterable 對象內部的迭代器,來不斷地進行迭代。此外,在 pReduce 函數中,使用了 「Promise.all」 方法,該方法會返回一個 promise 對象,當輸入的所有 promise 對象的狀態都變成 resolved 時,返回的 promise 對象就會以數組的形式,返回每個 promise 對象 resolve 後的結果。當輸入的任何一個 promise 對象狀態變成 rejected 時,則返回的 promise 對象會 reject 對應的錯誤信息。

不過,需要注意的是,「Promise.all」 方法存在兼容性問題,具體的兼容性如下圖所示:

(圖片來源 —— https://caniuse.com/?search=Promise.all)

可能有一些小夥伴對 「Promise.all」 還不熟悉,它又是一道比較高頻的手寫題。所以,接下來我們來手寫一個簡易版的 「Promise.all」

Promise.all = function (iterators) {
  return new Promise((resolve, reject) ={
    if (!iterators || iterators.length === 0) {
      resolve([]);
    } else {
      let count = 0; // 計數器,用於判斷所有任務是否執行完成
      let result = []; // 結果數組
      for (let i = 0; i < iterators.length; i++) {
        // 考慮到iterators[i]可能是普通對象,則統一包裝爲Promise對象
        Promise.resolve(iterators[i]).then(
          (data) ={
            result[i] = data; // 按順序保存對應的結果
            // 當所有任務都執行完成後,再統一返回結果
            if (++count === iterators.length) {
              resolve(result);
            }
          },
          (err) ={
            reject(err); // 任何一個Promise對象執行失敗,則調用reject()方法
            return;
          }
        );
      }
    }
  });
};

p-map

Map over promises concurrently

https://github.com/sindresorhus/p-map

使用說明

p-map 適用於使用不同的輸入多次運行 「promise-returning」「async」 函數的場景。與前面介紹的 Promise.all 方法的區別是,你可以控制併發,也可以決定是否在出現錯誤時停止迭代。該模塊默認導出了一個 「pMap」 函數,該函數的簽名如下:

「pMap(input, mapper, options): Promise」

瞭解完 「pMap」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-map/p-map.test.js
import delay from "delay";
import pMap from "p-map";

const inputs = [200, 100, 50];
const mapper = (value) => delay(value, { value });

async function main() {
  console.time("start");
  const result = await pMap(inputs, mapper, { concurrency: 1 });
  console.dir(result); // 輸出結果:[ 200, 100, 50 ]
  console.timeEnd("start");
}

main();

在以上示例中,我們也使用了 「delay」 模塊導出的 delay 函數,用於實現延遲功能。當成功執行以上代碼後,命令行會輸出以下信息:

[ 200, 100, 50 ]
start: 368.708ms

而當把 concurrency 屬性的值更改爲 2 之後,再次執行以上代碼。那麼命令行將會輸出以下信息:

[ 200, 100, 50 ]
start: 210.322ms

觀察以上的輸出結果,我們可以看出併發數爲 1 時,程序的運行時間大於 350ms。而如果併發數爲 2 時,多個任務是並行執行的,所以程序的運行時間只需 210 多毫秒。那麼 pMap 函數,內部是如何實現併發控制的呢?下面來分析一下 pMap 函數的源碼。

源碼分析

// https://github.com/sindresorhus/p-map/blob/main/index.js
import AggregateError from "aggregate-error";

export default async function pMap(
  iterable,
  mapper,
  { concurrency = Number.POSITIVE_INFINITY, stopOnError = true } = {}
) {
  return new Promise((resolve, reject) ={
    // 省略參數校驗代碼
    const result = []; // 存儲返回結果
    const errors = []; // 存儲異常對象
    const skippedIndexes = []; // 保存跳過項索引值的數組
    const iterator = iterable[Symbol.iterator](); // 獲取迭代器
    let isRejected = false; // 標識是否出現異常
    let isIterableDone = false; // 標識是否已迭代完成
    let resolvingCount = 0; // 正在處理的任務個數
    let currentIndex = 0; // 當前索引

    const next = () ={
      if (isRejected) { // 若出現異常,則直接返回
        return;
      }

      const nextItem = iterator.next(); // 獲取下一項
      const index = currentIndex; // 記錄當前的索引值
      currentIndex++;

      if (nextItem.done) { // 判斷迭代器是否迭代完成
        isIterableDone = true;

        // 判斷是否所有的任務都已經完成了
        if (resolvingCount === 0) { 
          if (!stopOnError && errors.length > 0) { // 異常處理
            reject(new AggregateError(errors));
          } else {
            for (const skippedIndex of skippedIndexes) {
              // 刪除跳過的值,不然會存在空的佔位
              result.splice(skippedIndex, 1); 
            }
            resolve(result); // 返回最終的處理結果
          }
        }
        return;
      }

      resolvingCount++; // 正在處理的任務數加1

      (async () ={
        try {
          const element = await nextItem.value;

          if (isRejected) {
            return;
          }

          // 調用mapper函數,進行值進行處理
          const value = await mapper(element, index);
          // 處理跳過的情形,可以在mapper函數中返回pMapSkip,來跳過當前項
          // 比如在異常捕獲的catch語句中,返回pMapSkip值
          if (value === pMapSkip) { // pMapSkip = Symbol("skip")
            skippedIndexes.push(index);
          } else {
            result[index] = value; // 把返回值按照索引進行保存
          }

          resolvingCount--;
          next(); // 迭代下一項
        } catch (error) {
          if (stopOnError) { // 出現異常時,是否終止,默認值爲true
            isRejected = true;
            reject(error);
          } else {
            errors.push(error);
            resolvingCount--;
            next();
          }
        }
      })();
    };

    // 根據配置的concurrency值,併發執行任務
    for (let index = 0; index < concurrency; index++) {
      next();
      if (isIterableDone) {
        break;
      }
    }
  });
}

export const pMapSkip = Symbol("skip");

pMap 函數內部的處理邏輯還是蠻清晰的,把核心的處理邏輯都封裝在 next 函數中。在調用 pMap 函數之後,內部會根據配置的concurrency 值,併發調用 next 函數。而在 next 函數內部,會通過 「async/await」 來控制任務的執行過程。

pMap 函數中,作者巧妙設計了 「pMapSkip」。當我們在 mapper 函數中返回了 「pMapSkip」 之後,將會從返回的結果數組中移除對應索引項的值。瞭解完 「pMapSkip」 的作用之後,我們來舉個簡單的例子:

import pMap, { pMapSkip } from "p-map";

const inputs = [200, pMapSkip, 50];
const mapper = async (value) => value;

async function main() {
  console.time("start");
  const result = await pMap(inputs, mapper, { concurrency: 2 });
  console.dir(result); // [ 200, 50 ]
  console.timeEnd("start");
}

main();

在以上代碼中,我們的 inputs 數組中包含了 pMapSkip 值,當使用 pMap 函數對 inputs 數組進行處理後,pMapSkip 值將會被過濾掉,所以最終 result 的結果爲 「[200 , 50]」

p-filter

Filter promises concurrently

https://github.com/sindresorhus/p-filter

使用說明

p-filter 適用於使用不同的輸入多次運行 「promise-returning」「async」 函數,並對返回的結果進行過濾的場景。該模塊默認導出了一個 「pFilter」 函數,該函數的簽名如下:

「pFilter(input, filterer, options): Promise」

瞭解完 「pFilter」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-filter/p-filter.test.js
import pFilter from "p-filter";

const inputs = [Promise.resolve(1), 2, 3];
const filterer = (x) => x % 2;

async function main() {
  const result = await pFilter(inputs, filterer, { concurrency: 1 });
  console.dir(result); // 輸出結果:[ 1, 3 ]
}

main();

在以上示例中,我們使用 pFilter 函數對包含 Promise 對象的 inputs 數組,應用了 (x) => x % 2 過濾器。當以上代碼成功運行後,命令行會輸出 「[1, 3]」

源碼分析

// https://github.com/sindresorhus/p-filter/blob/main/index.js
const pMap = require('p-map');

const pFilter = async (iterable, filterer, options) ={
 const values = await pMap(
  iterable,
  (element, index) => Promise.all([filterer(element, index), element]),
  options
 );
 return values.filter(value => Boolean(value[0])).map(value => value[1]);
};

由以上代碼可知,在 pFilter 函數內部,使用了我們前面已經介紹過的 pMapPromise.all 函數。要理解以上代碼,我們還需要來回顧一下 pMap 函數的關鍵代碼:

// https://github.com/sindresorhus/p-map/blob/main/index.js
export default async function pMap(
  iterable, mapper,
  { concurrency = Number.POSITIVE_INFINITY, stopOnError = true } = {}
) {
  const iterator = iterable[Symbol.iterator](); // 獲取迭代器
  let currentIndex = 0; // 當前索引
  
  const next = () ={
    const nextItem = iterator.next(); // 獲取下一項
    const index = currentIndex;
    currentIndex++;
    (async () ={
        try {
          // element => await Promise.resolve(1);
          const element = await nextItem.value;
          // mapper =(element, index) => Promise.all([filterer(element, index), element])
          const value = await mapper(element, index);
          if (value === pMapSkip) {
            skippedIndexes.push(index);
          } else {
            result[index] = value; // 把返回值按照索引進行保存
          }
          resolvingCount--;
          next(); // 迭代下一項
        } catch (error) {
          // 省略異常處理代碼
        }
    })();
  } 
}

因爲 pFilter 函數中所用的 mapper 函數是 (element, index) => Promise.all([filterer(element, index), element]),所以 await mapper(element, index) 表達式的返回值是一個數組。數組的第 1 項是 filterer 過濾器的處理結果,而數組的第 2 項是當前元素的值。因此在調用 pMap 函數後,它的返回值是一個二維數組。所以在獲取 pMap 函數的返回值之後, 會使用以下語句對返回值進行處理:

values.filter(value => Boolean(value[0])).map(value => value[1])

其實,對於前面的 pFilter 示例來說,除了 inputs 可以含有 Promise 對象,我們的 filterer 過濾器也可以返回 Promise 對象:

import pFilter from "p-filter";

const inputs = [Promise.resolve(1), 2, 3];
const filterer = (x) => Promise.resolve(x % 2);

async function main() {
  const result = await pFilter(inputs, filterer);
  console.dir(result); // [ 1, 3 ]
}

main();

以上代碼成功執行後,命令行的輸出結果也是 「[1, 3]」。好的,現在我們已經介紹了 p-reduce、p-map 和 p-filter 3 個模塊。下面我們來繼續介紹另一個模塊 —— p-waterfall。

p-waterfall

Run promise-returning & async functions in series, each passing its result to the next

https://github.com/sindresorhus/p-waterfall

使用說明

p-waterfall 適用於串行執行 「promise-returning」「async」 函數,並把前一個函數的返回結果自動傳給下一個函數的場景。該模塊默認導出了一個 「pWaterfall」 函數,該函數的簽名如下:

「pWaterfall(tasks, initialValue): Promise」

瞭解完 「pWaterfall」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-waterfall/p-waterfall.test.js
import pWaterfall from "p-waterfall";

const tasks = [
  async (val) => val + 1,
  (val) => val + 2,
  async (val) => val + 3,
];

async function main() {
  const result = await pWaterfall(tasks, 0);
  console.dir(result); // 輸出結果:6
}

main();

在以上示例中,我們創建了 3 個任務,然後使用 pWaterfall 函數來執行這 3 個任務。當以上代碼成功執行後,命令行會輸出 「6」。對應的執行流程如下圖所示:

源碼分析

// https://github.com/sindresorhus/p-waterfall/blob/main/index.js
import pReduce from 'p-reduce';

export default async function pWaterfall(iterable, initialValue) {
 return pReduce(iterable, (previousValue, function_) => function_(previousValue), initialValue);
}

pWaterfall 函數內部,會利用前面已經介紹的 pReduce 函數來實現 「waterfall」 流程控制。同樣,要搞清楚內部的控制流程,我們需要來回顧一下 pReduce 函數的具體實現:

export default async function pReduce(iterable, reducer, initialValue) {
  return new Promise((resolve, reject) ={
    const iterator = iterable[Symbol.iterator](); // 獲取迭代器
    let index = 0; // 索引值

    const next = async (total) ={
      const element = iterator.next(); // 獲取下一項

      if (element.done) {
        // 判斷迭代器是否迭代完成
        resolve(total);
        return;
      }

      try {
        // 首次調用next函數的狀態:
        // resolvedTotal =0
        // element.value => async (val) => val + 1
        const [resolvedTotal, resolvedValue] = await Promise.all([
          total,
          element.value,
        ]);
        // reducer =(previousValue, function_) => function_(previousValue)
        next(reducer(resolvedTotal, resolvedValue, index++));
      } catch (error) {
        reject(error);
      }
    };

    // 使用初始值,開始迭代
    next(initialValue);
  });
}

現在我們已經知道 「pWaterfall」 函數會把前一個任務的輸出結果,作爲輸入傳給下一個任務。但有些時候,在串行執行每個任務時,我們並不關心每個任務的返回值。針對這種場合,我們可以考慮使用 p-series 模塊提供的 pSeries 函數。

p-series

Run promise-returning & async functions in series

https://github.com/sindresorhus/p-series

使用說明

p-series 適用於串行執行 「promise-returning」「async」 函數的場景。

使用示例

// p-series/p-series.test.js
import pSeries from "p-series";

const tasks = [async () => 1 + 1, () => 2 + 2, async () => 3 + 3];

async function main() {
  const result = await pSeries(tasks);
  console.dir(result); // 輸出結果:[2, 4, 6]
}

main();

在以上示例中,我們創建了 3 個任務,然後使用 pSeries 函數來執行這 3 個任務。當以上代碼成功執行後,命令行會輸出 「[2, 4, 6]」。對應的執行流程如下圖所示:

源碼分析

// https://github.com/sindresorhus/p-series/blob/main/index.js
export default async function pSeries(tasks) {
 for (const task of tasks) {
  if (typeof task !== 'function') {
   throw new TypeError(`Expected task to be a \`Function\`, received \`${typeof task}\``);
  }
 }

 const results = [];

 for (const task of tasks) {
  results.push(await task()); // eslint-disable-line no-await-in-loop
 }

 return results;
}

由以上代碼可知,在 pSeries 函數內部是利用 「for...of」 語句和 「async/await」 特性來實現串行任務流控制。因此在實際的項目中,你也可以無需使用該模塊,就可以輕鬆的實現串行任務流控制。

p-all

Run promise-returning & async functions concurrently with optional limited concurrency

https://github.com/sindresorhus/p-all

使用說明

p-all 適用於併發執行 「promise-returning」「async」 函數的場景。該模塊提供的功能,與 「Promise.all」 API 類似,主要的區別是該模塊允許你限制任務的併發數。在日常開發過程中,一個比較常見的場景就是控制 HTTP 請求的併發數,這時你也可以考慮使用 async-pool 這個庫來解決併發控制的問題,如果你對該庫的內部實現原理感興趣的話,可以閱讀 JavaScript 中如何實現併發控制? 這篇文章。

下面我們來繼續介紹 p-all 模塊,該模塊默認導出了一個 「pAll」 函數,該函數的簽名如下:

「pAll(tasks, options)」

使用示例

// p-all/p-all.test.js
import delay from "delay";
import pAll from "p-all";

const inputs = [
  () => delay(200, { value: 1 }),
  async () ={
    await delay(100);
    return 2;
  },
  async () => 8,
];

async function main() {
  console.time("start");
  const result = await pAll(inputs, { concurrency: 1 });
  console.dir(result); // 輸出結果:[ 1, 2, 8 ]
  console.timeEnd("start");
}

main();

在以上示例中,我們創建了 3 個異步任務,然後通過 pAll 函數來執行已創建的任務。當成功執行以上代碼後,命令行會輸出以下信息:

[ 1, 2, 8 ]
start: 312.561ms

而當把 concurrency 屬性的值更改爲 2 之後,再次執行以上代碼。那麼命令行將會輸出以下信息:

[ 1, 2, 8 ]
start: 209.469ms

可以看出併發數爲 1 時,程序的運行時間大於 300ms。而如果併發數爲 2 時,前面兩個任務是並行的,所以程序的運行時間只需 200 多毫秒。

源碼分析

// https://github.com/sindresorhus/p-all/blob/main/index.js
import pMap from 'p-map';

export default async function pAll(iterable, options) {
 return pMap(iterable, element => element(), options);
}

很明顯在 pAll 函數內部,是通過 p-map 模塊提供的 pMap 函數來實現併發控制的。如果你對 pMap 函數的內部實現方式,還不清楚的話,可以回過頭再次閱讀 p-map 模塊的相關內容。接下來,我們來繼續介紹另一個模塊 —— p-race。

p-race

A better Promise.race()

https://github.com/sindresorhus/p-race

使用說明

p-race 這個模塊修復了 Promise.race API 一個 “愚蠢” 的行爲。當使用空的可迭代對象,調用 Promise.race API 時,將會返回一個永遠處於 「pending」 狀態的 Promise 對象,這可能會產生一些非常難以調試的問題。而如果往 p-race 模塊提供的 pRace 函數中傳入一個空的可迭代對象時,該函數將會立即拋出 「RangeError: Expected the iterable to contain at least one item」 的異常信息。

pRace(iterable) 方法會返回一個 promise 對象,一旦迭代器中的某個 promise 對象 「resolved」「rejected」,返回的 promise 對象就會 resolve 或 reject 相應的值。

使用示例

// p-race/p-race.test.js
import delay from "delay";
import pRace from "p-race";

const inputs = [delay(50, { value: 1 }), delay(100, { value: 2 })];

async function main() {
  const result = await pRace(inputs);
  console.dir(result); // 輸出結果:1
}

main();

在以上示例中,我們導入了 「delay」 模塊默認導出的 delay 方法,該方法可用於按照給定的時間,延遲一個 Promise 對象。利用 delay 函數,我們創建了 2 個 Promise 對象,然後使用 pRace 函數來處理這兩個 Promise 對象。以上代碼成功運行後,命令行始終會輸出 「1」。那麼爲什麼會這樣呢?下面我們來分析一下 pRace 函數的源碼。

源碼分析

// https://github.com/sindresorhus/p-race/blob/main/index.js
import isEmptyIterable from 'is-empty-iterable';

export default async function pRace(iterable) {
 if (isEmptyIterable(iterable)) {
  throw new RangeError('Expected the iterable to contain at least one item');
 }

 return Promise.race(iterable);
}

觀察以上源碼可知,在 pRace 函數內部會先判斷傳入的 iterable 參數是否爲空的可迭代對象。檢測參數是否爲空的可迭代對象,是通過 isEmptyIterable 函數來實現,該函數的具體代碼如下所示:

// https://github.com/sindresorhus/is-empty-iterable/blob/main/index.js
function isEmptyIterable(iterable) {
 for (const _ of iterable) {
  return false;
 }

 return true;
}

當發現是空的可迭代對象時,pRace 函數會直接拋出 RangeError 異常。否則,會利用 Promise.race API 來實現具體的功能。需要注意的是,「Promise.race」 方法也存在兼容性問題,具體如下圖所示:

(圖片來源 —— https://caniuse.com/?search=Promise.race)

同樣,可能有一些小夥伴對 「Promise.race」 還不熟悉,它也是一道挺高頻的手寫題。所以,接下來我們來手寫一個簡易版的 「Promise.race」

Promise.race = function (iterators) {
  return new Promise((resolve, reject) ={
    for (const iter of iterators) {
      Promise.resolve(iter)
        .then((res) ={
          resolve(res);
        })
        .catch((e) ={
          reject(e);
        });
    }
  });
};

p-forever

Run promise-returning & async functions repeatedly until you end it

https://github.com/sindresorhus/p-forever

使用說明

p-forever 適用於需要重複不斷執行 「promise-returning」「async」 函數,直到用戶終止的場景。該模塊默認導出了一個 「pForever」 函數,該函數的簽名如下:

「pForever(fn, initialValue)」

瞭解完 「pForever」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-forever/p-forever.test.js
import delay from "delay";
import pForever from "p-forever";

async function main() {
  let index = 0;
  await pForever(async () =(++index === 10 ? pForever.end : delay(50)));
  console.log("當前index的值: ", index); // 輸出結果:當前index的值: 10
}

main();

在以上示例中,傳入 pForever 函數的 fn 函數會一直重複執行,直到該 fn 函數返回 pForever.end 的值,纔會終止執行。因此以上代碼成功執行後,命令行的輸出結果是:「當前 index 的值:  10」

源碼分析

// https://github.com/sindresorhus/p-forever/blob/main/index.js
const endSymbol = Symbol('pForever.end');

const pForever = async (function_, previousValue) ={
 const newValue = await function_(await previousValue);
 if (newValue === endSymbol) {
  return;
 }
 return pForever(function_, newValue);
};

pForever.end = endSymbol;
export default pForever;

由以上源碼可知,pForever 函數的內部實現並不複雜。當判斷 newValue 的值爲 endSymbol 時,就直接返回了。否則,就會繼續調用 pForever 函數。除了一直重複執行任務之外,有時候我們會希望顯式指定任務的執行次數,針對這種場景,我們就可以使用 p-times 模塊。

p-times

Run promise-returning & async functions a specific number of times concurrently

https://github.com/sindresorhus/p-times

使用說明

p-times 適用於顯式指定 「promise-returning」「async」 函數執行次數的場景。該模塊默認導出了一個 「pTimes」 函數,該函數的簽名如下:

「pTimes(count, mapper, options): Promise」

瞭解完 「pTimes」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-times/p-times.test.js
import delay from "delay";
import pTimes from "p-times";

async function main() {
  console.time("start");
  const result = await pTimes(5, async (i) => delay(50, { value: i * 10 }){
    concurrency: 3,
  });
  console.dir(result);
  console.timeEnd("start");
}

main();

在以上示例中,我們通過 pTimes 函數配置 「mapper」 函數的執行次數爲 「5」 次,同時設置任務的併發數爲 「3」。當以上代碼成功運行後,命令行會輸出以下結果:

[ 0, 10, 20, 30, 40 ]
start: 116.090ms

對於以上示例,你可以通過改變 concurrency 的值,來對比輸出的程序運行時間。那麼 pTimes 函數內部是如何實現併發控制的呢?其實該函數內部也是利用 pMap 函數來實現併發控制。

源碼分析

// https://github.com/sindresorhus/p-times/blob/main/index.js
import pMap from "p-map";

export default function pTimes(count, mapper, options) {
  return pMap(
    Array.from({ length: count }).fill(),
    (_, index) => mapper(index),
    options
  );
}

pTimes 函數中,會通過 Array.from 方法創建指定長度的數組,然後通過 fill 方法進行填充。最後再把該數組、mapper 函數和 options 配置對象,作爲輸入參數調用 pMap 函數。寫到這裏,阿寶哥覺得 pMap 函數提供的功能還是蠻強大的,很多模塊的內部都使用了 pMap 函數。

p-pipe

Compose promise-returning & async functions into a reusable pipeline

https://github.com/sindresorhus/p-pipe

使用說明

p-pipe 適用於把 「promise-returning」「async」 函數組合成可複用的管道。該模塊默認導出了一個 「pPipe」 函數,該函數的簽名如下:

「pPipe(input...)」

瞭解完 「pPipe」 函數的簽名之後,我們來看一下該函數如何使用。

使用示例

// p-pipe/p-pipe.test.js
import pPipe from "p-pipe";

const addUnicorn = async (string) =`${string} Unicorn`;
const addRainbow = async (string) =`${string} Rainbow`;

const pipeline = pPipe(addUnicorn, addRainbow);

(async () ={
  console.log(await pipeline("❤️")); // 輸出結果:❤️ Unicorn Rainbow
})();

在以上示例中,我們通過 pPipe 函數把 addUnicornaddRainbow 這兩個函數組合成一個新的可複用的管道。被組合函數的執行順序是從左到右,所以以上代碼成功運行後,命令行會輸出 「❤️ Unicorn Rainbow」

源碼分析

// https://github.com/sindresorhus/p-pipe/blob/main/index.js
export default function pPipe(...functions) {
 if (functions.length === 0) {
  throw new Error('Expected at least one argument');
 }

 return async input ={
  let currentValue = input;

  for (const function_ of functions) {
   currentValue = await function_(currentValue); // eslint-disable-line no-await-in-loop
  }
  return currentValue;
 };
}

由以上代碼可知,在 pPipe 函數內部是利用 「for...of」 語句和 「async/await」 特性來實現管道的功能。分析完 promise-fun 項目中的 10 個模塊之後,再次感受到 「async/await」 特性給前端異步編程帶來的巨大便利。其實對於異步的場景來說,除了可以使用 promise-fun 項目中收錄的模塊之外,還可以使用 async 或 neo-async 這兩個異步處理模塊提供的工具函數。在 Webpack 項目中,就用到了 neo-async 這個模塊,該模塊的作者是希望用來替換 async 模塊,以提供更好的性能。建議需要經常處理異步場景的小夥伴,抽空瀏覽一下 neo-async 這個模塊的 「官方文檔」

總結

promise-fun 項目共收錄了 「50」 個與 Promise 有關的模塊,該項目的作者 「sindresorhus」 個人就開發了 「48」 個模塊,不愧是全職做開源的大牛。由於篇幅有限,阿寶哥只介紹了其中 「10」 個比較常用的模塊。其實該項目還包含一些挺不錯的模塊,比如 「p-queue」「p-any」「p-some」「p-debounce」「p-throttle」「p-timeout」 等。感興趣的小夥伴,可以自行了解一下其他的模塊。

參考資源

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BfhLEd8U_lRTUF1j1UEn6w