如何正确的在 Array.map 使用 async

封面

map 中返回Promises,然后等待结果

本文译自How to use async functions with Array.map in Javascript - Tamás Sallai

在前面的文章中,我们介绍了 async / await如何帮助执行异步命令 ,但在异步处理集合时却无济于事。在本文中,我们将研究该map函数,该函数是最常用的函数,它将数据从一种形式转换为另一种形式(这里可以理解为 map具有返回值)。

1. Array.map

map是最简单和最常见的采集功能。它通过迭代函数运行每个元素,并返回包含结果的数组。

向每个元素添加一的同步版本:

const arr = [1, 2, 3];

const syncRes = arr.map((i) => {
    return i + 1;
});

console.log(syncRes);
// 2,3,4

异步版本需要做两件事。首先,它需要将每个项目映射到具有新值的 Promise,这是async在函数执行之前添加的内容。

其次,它需要等待所有Promises,然后将结果收集到Array中。幸运的是,Promise.all内置调用正是我们执行步骤2所需的。

这使得一个异步的一般模式mapPromise.all(arr.map(async (...) => ...))

异步实现与同步实现相同:

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. 并发

上面的实现为数组的每个元素并行运行迭代函数。通常这很好,但是在某些情况下,它可能会消耗过多的资源。当异步函数访问 API 或消耗过多的RAM以至于无法一次运行太多RAM时,可能会发生这种情况。

尽管异步map易于编写,但要增加并发控件。在接下来的几个示例中,我们将研究不同的解决方案。

2.1 批量处理

最简单的方法是对元素进行分组并逐个处理。这使您可以控制一次可以运行的最大并行任务数。但是由于一组必须在下一组开始之前完成,因此每组中最慢的元素成为限制因素。

为了进行分组,下面的示例使用Underscore.jsgroupBy实现。许多库提供了一种实现,并且它们大多数都是可互换的。Lodash是个例外,因为其 groupBy 不传递 item的索引。

如果您不熟悉groupBy,它将通过迭代函数运行每个元素,并返回一个对象,其键为结果,值为产生该值的元素的列表。

为了使群体最多n的元素,一个迭代器 Math.floor(i / n),其中 i 是元素的索引。例如,一组大小为3的元素将映射以下元素:

0 => 0
1 => 0
2 => 0
3 => 1
4 => 1
5 => 1
6 => 2
...

Javascript实现:

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(...))构造是很好。

要按顺序映射组,我们需要一个reduce,它将先前的结果(memo)与当前组的结果连接起来:

return Object.values(groups)
    .reduce(async (memo, group) => [
        ...(await memo),
        ...(await Promise.all(group.map(iteratee)))
    ], []);

此实现基于以下事实:await memo等待上一个结果的完成才进行下一个任务。

实现批处理的完整实现:

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 并行处理

并发控制的另一种类型是并行执行大多数n任务,并在完成一项任务时启动一个新任务。

我无法为此提供一个简单的实现,但是幸运的是,Bluebird提供了一个开箱即用的库。这很简单,只需导入库并使用Promise.map支持该concurrency选项的功能即可。

在下面的示例中,并发限制为2,这意味着立即启动2个任务,然后每完成一个任务,就开始一个新任务,直到没有剩余:

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 顺序处理

有时,并发太多,因此应该一个接一个地处理元素。

一个简单的实现是使用并发性为 1 的 BluebirdPromise。但是在这种情况下,它不保证包括一个库,因为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

确保在执行任何其他操作之前 await memo,因为如果没有 await,它仍然会并发运行!

3. 结论

map功能很容易转换为异步,因为Promise.all内置功能繁重。但是控制并发需要一些计划。

推荐阅读

如果对你有所帮助,可以点赞、收藏。

您的关注是莫大的鼓励 ❥(^_-)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容