
在
map中返回Promises,然后等待結(jié)果本文譯自How to use async functions with Array.map in Javascript - Tamás Sallai 。
在前面的文章中,我們介紹了 async / await如何幫助執(zhí)行異步命令 ,但在異步處理集合時卻無濟于事。在本文中,我們將研究該map函數(shù),該函數(shù)是最常用的函數(shù),它將數(shù)據(jù)從一種形式轉(zhuǎn)換為另一種形式(這里可以理解為 map具有返回值)。
1. Array.map
該map是最簡單和最常見的采集功能。它通過迭代函數(shù)運行每個元素,并返回包含結(jié)果的數(shù)組。
向每個元素添加一的同步版本:
const arr = [1, 2, 3];
const syncRes = arr.map((i) => {
return i + 1;
});
console.log(syncRes);
// 2,3,4
異步版本需要做兩件事。首先,它需要將每個項目映射到具有新值的 Promise,這是async在函數(shù)執(zhí)行之前添加的內(nèi)容。
其次,它需要等待所有Promises,然后將結(jié)果收集到Array中。幸運的是,Promise.all內(nèi)置調(diào)用正是我們執(zhí)行步驟2所需的。
這使得一個異步的一般模式map是Promise.all(arr.map(async (...) => ...))。
異步實現(xiàn)與同步實現(xiàn)相同:
const arr = [1, 2, 3];
const asyncRes = await Promise.all(arr.map(async (i) => {
await sleep(10);
return i + 1;
}));
console.log(asyncRes);
// 2,3,4

2. 并發(fā)
上面的實現(xiàn)為數(shù)組的每個元素并行運行迭代函數(shù)。通常這很好,但是在某些情況下,它可能會消耗過多的資源。當異步函數(shù)訪問 API 或消耗過多的RAM以至于無法一次運行太多RAM時,可能會發(fā)生這種情況。
盡管異步map易于編寫,但要增加并發(fā)控件。在接下來的幾個示例中,我們將研究不同的解決方案。
2.1 批量處理
最簡單的方法是對元素進行分組并逐個處理。這使您可以控制一次可以運行的最大并行任務(wù)數(shù)。但是由于一組必須在下一組開始之前完成,因此每組中最慢的元素成為限制因素。

為了進行分組,下面的示例使用Underscore.js的groupBy實現(xiàn)。許多庫提供了一種實現(xiàn),并且它們大多數(shù)都是可互換的。Lodash是個例外,因為其 groupBy 不傳遞 item的索引。
如果您不熟悉groupBy,它將通過迭代函數(shù)運行每個元素,并返回一個對象,其鍵為結(jié)果,值為產(chǎn)生該值的元素的列表。
為了使群體最多n的元素,一個迭代器 Math.floor(i / n),其中 i 是元素的索引。例如,一組大小為3的元素將映射以下元素:
0 => 0
1 => 0
2 => 0
3 => 1
4 => 1
5 => 1
6 => 2
...
Javascript實現(xiàn):
const arr = [30, 10, 20, 20, 15, 20, 10];
console.log(
_.groupBy(arr, (_v, i) => Math.floor(i / 3))
);
// {
// 0: [30, 10, 20],
// 1: [20, 15, 20],
// 2: [10]
// }
最后一組可能比其他組小,但是保證所有組都不會超過最大組大小。
要映射一組,通常的Promise.all(group.map(...))構(gòu)造是很好。
要按順序映射組,我們需要一個reduce,它將先前的結(jié)果(memo)與當前組的結(jié)果連接起來:
return Object.values(groups)
.reduce(async (memo, group) => [
...(await memo),
...(await Promise.all(group.map(iteratee)))
], []);
此實現(xiàn)基于以下事實:await memo等待上一個結(jié)果的完成才進行下一個任務(wù)。
實現(xiàn)批處理的完整實現(xiàn):
const arr = [30, 10, 20, 20, 15, 20, 10];
const mapInGroups = (arr, iteratee, groupSize) => {
const groups = _.groupBy(arr, (_v, i) => Math.floor(i / groupSize));
return Object.values(groups)
.reduce(async (memo, group) => [
...(await memo),
...(await Promise.all(group.map(iteratee)))
], []);
};
const res = await mapInGroups(arr, async (v) => {
console.log(`S ${v}`);
await sleep(v);
console.log(`F ${v}`);
return v + 1;
}, 3);
// -- first batch --
// S 30
// S 10
// S 20
// F 10
// F 20
// F 30
// -- second batch --
// S 20
// S 15
// S 20
// F 15
// F 20
// F 20
// -- third batch --
// S 10
// F 10
console.log(res);
// 31,11,21,21,16,21,11
2.2 并行處理
并發(fā)控制的另一種類型是并行執(zhí)行大多數(shù)n任務(wù),并在完成一項任務(wù)時啟動一個新任務(wù)。

我無法為此提供一個簡單的實現(xiàn),但是幸運的是,Bluebird提供了一個開箱即用的庫。這很簡單,只需導入庫并使用Promise.map支持該concurrency選項的功能即可。
在下面的示例中,并發(fā)限制為2,這意味著立即啟動2個任務(wù),然后每完成一個任務(wù),就開始一個新任務(wù),直到?jīng)]有剩余:
const arr = [30, 10, 20, 20, 15, 20, 10];
// Bluebird promise
const res = await Promise.map(arr, async (v) => {
console.log(`S ${v}`)
await sleep(v);
console.log(`F ${v}`);
return v + 1;
}, {concurrency: 2});
// S 30
// S 10
// F 10
// S 10
// F 30
// S 20
// F 10
// S 15
// F 20
// S 20
// F 15
// S 20
// F 20
// F 20
console.log(res);
// 31,11,21,21,16,21,11
2.3 順序處理
有時,并發(fā)太多,因此應(yīng)該一個接一個地處理元素。

一個簡單的實現(xiàn)是使用并發(fā)性為 1 的 Bluebird 的 Promise。但是在這種情況下,它不保證包括一個庫,因為reduce這樣做很簡單:
const arr = [1, 2, 3];
const res = await arr.reduce(async (memo, v) => {
const results = await memo;
console.log(`S ${v}`)
await sleep(10);
console.log(`F ${v}`);
return [...results, v + 1];
}, []);
// S 1
// F 1
// S 2
// F 2
// S 3
// F 3
console.log(res);
// 2,3,4
確保在執(zhí)行任何其他操作之前 await memo,因為如果沒有 await,它仍然會并發(fā)運行!
3. 結(jié)論
該map功能很容易轉(zhuǎn)換為異步,因為Promise.all內(nèi)置功能繁重。但是控制并發(fā)需要一些計劃。
推薦閱讀
如果對你有所幫助,可以點贊、收藏。
