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",
...
},
}
在完成項目初始化之後,我們先來回顧一下大家平時用得比較多的 reduce
、map
和 filter
數組方法的特點:
❝
提示:上圖通過👉 「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 個參數:
-
acc(Accumulator):累計器
-
cur(Current Value):當前值
-
idx(Current Index):當前索引
-
src(Source Array):源數組
而接下來,我們要介紹的 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」
❞
-
input: Iterable<Promise|any>
-
reducer(previousValue, currentValue, index): Function
-
initialValue: unknown
瞭解完 「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」
❞
-
input: Iterable<Promise | unknown>
-
mapper(element, index): Function
-
options: object
-
concurrency: number
—— 併發數,默認值Infinity
,最小值爲1
; -
stopOnError: boolean
—— 出現異常時,是否終止,默認值爲true
。
瞭解完 「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」
❞
-
input: Iterable<Promise | any>
-
filterer(element, index): Function
-
options: object
-
concurrency: number
—— 併發數,默認值Infinity
,最小值爲1
。
瞭解完 「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
函數內部,使用了我們前面已經介紹過的 pMap
和 Promise.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」
❞
-
tasks: Iterable<Function>
-
initialValue: unknown
:將作爲第一個任務的previousValue
瞭解完 「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)」
❞
-
tasks: Iterable<Function>
-
options: object
-
concurrency: number
—— 併發數,默認值Infinity
,最小值爲1
; -
stopOnError: boolean
—— 出現異常時,是否終止,默認值爲true
。
使用示例
// 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)」
❞
-
fn: Function
:重複執行的函數; -
initialValue
:傳遞給fn
函數的初始值。
瞭解完 「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」
❞
-
count: number
:調用次數; -
mapper(index): Function
:mapper 函數,調用該函數後會返回 Promise 對象或某個具體的值; -
options: object
-
concurrency: number
—— 併發數,默認值Infinity
,最小值爲1
; -
stopOnError: boolean
—— 出現異常時,是否終止,默認值爲true
。
瞭解完 「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...)」
❞
input: Function
:期望調用後會返回 Promise 或任何值的函數。
瞭解完 「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
函數把 addUnicorn
和 addRainbow
這兩個函數組合成一個新的可複用的管道。被組合函數的執行順序是從左到右,所以以上代碼成功運行後,命令行會輸出 「❤️ 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」 等。感興趣的小夥伴,可以自行了解一下其他的模塊。
參考資源
-
MDN — Array.prototype.reduce()
-
MDN — Array.from
-
Promise.race with empty lists
-
neo-async 官方文檔
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/BfhLEd8U_lRTUF1j1UEn6w