美文网首页
如何正确的在 Array.map 使用 async

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

作者: 震雨Rain | 来源:发表于2020-04-22 10:34 被阅读0次
    封面

    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内置功能繁重。但是控制并发需要一些计划。

    推荐阅读

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

    您的关注是莫大的鼓励 ❥(^_-)

    相关文章

      网友评论

          本文标题:如何正确的在 Array.map 使用 async

          本文链接:https://www.haomeiwen.com/subject/ruvbihtx.html