使用 Array.map 的异步等待

Use async await with Array.map

给定以下代码:

var arr = [1,2,3,4,5];

var results: number[] = await arr.map(async (item): Promise<number> => {
        await callAsynchronousOperation(item);
        return item + 1;
    });

产生以下错误:

TS2322: Type 'Promise<number>[]' is not assignable to type 'number[]'. Type 'Promise<number> is not assignable to type 'number'.

我该如何解决?如何让 async awaitArray.map 一起工作?

如果映射到 Promise 数组,则可以将它们全部解析为数字数组。参见 Promise.all

这里的问题是您正在尝试 await 一组承诺而不是一个承诺。这不符合您的预期。

当传递给 await 的对象不是 Promise 时,await 只是立即 returns 原样的值,而不是尝试解析它。因此,由于您在此处传递了 await 一个(Promise 对象的)数组而不是 Promise,因此 await 返回的值只是该数组,其类型为 Promise<number>[].

您可能想要做的是在 map 返回的数组上调用 Promise.all,以便在 awaiting 之前将其转换为单个 Promise。

根据 MDN docs for Promise.all:

The Promise.all(iterable) method returns a promise that resolves when all of the promises in the iterable argument have resolved, or rejects with the reason of the first passed promise that rejects.

所以在你的情况下:

var arr = [1, 2, 3, 4, 5];

var results: number[] = await Promise.all(arr.map(async (item): Promise<number> => {
    await callAsynchronousOperation(item);
    return item + 1;
}));

这将解决您在这里遇到的特定错误。

根据您要执行的操作,您也可以考虑使用 Promise.allSettled, Promise.any, or Promise.race 而不是 Promise.all,但在大多数情况下(几乎肯定包括这个)Promise.all 会是你想要的。

我建议使用上面提到的 Promise.all,但如果您真的想避免这种方法,您可以执行 for 或任何其他循环:

const arr = [1,2,3,4,5];
let resultingArr = [];
for (let i in arr){
  await callAsynchronousOperation(i);
  resultingArr.push(i + 1)
}

仅供参考:如果要遍历数组的项目而不是索引(@ralfoide 的评论),请使用 of 而不是 inlet i in arr 语句中。

如果您不使用本机 Promises 而使用 Bluebird,则还有另一种解决方案。

您也可以尝试使用 Promise.map(),混合使用 array.map 和 Promise.all

在你的情况下:

  var arr = [1,2,3,4,5];

  var results: number[] = await Promise.map(arr, async (item): Promise<number> => {
    await callAsynchronousOperation(item);
    return item + 1;
  });

下面的解决方案可以正确地同时使用 async await 和 Array.map。并行、异步处理数组的所有元素并保留顺序:

const arr = [1, 2, 3, 4, 5, 6, 7, 8];
const randomDelay = () => new Promise(resolve => setTimeout(resolve, Math.random() * 1000));

const calc = async n => {
  await randomDelay();
  return n * 2;
};

const asyncFunc = async () => {
  const unresolvedPromises = arr.map(n => calc(n));
  const results = await Promise.all(unresolvedPromises);
};

asyncFunc();

还有codepen.

注意我们只“等待”Promise.all。我们在没有“等待”的情况下多次调用 calc,我们立即收集了一系列未解决的承诺。然后 Promise.all 等待所有这些的解析和 returns 一个数组,其中包含按顺序解析的值。

我在 BE 方面有一项任务是从 repo 中找到所有实体,并添加一个新的 属性 url 和 return 到控制器层。我是这样实现的(感谢 Ajedi32 的回复):

async findAll(): Promise<ImageResponse[]> {
    const images = await this.imageRepository.find(); // This is an array of type Image (DB entity)
    const host = this.request.get('host');
    const mappedImages = await Promise.all(images.map(image => ({...image, url: `http://${host}/images/${image.id}`}))); // This is an array of type Object
    return plainToClass(ImageResponse, mappedImages); // Result is an array of type ImageResponse
  }

注意:图像(实体)没有属性 url,但是ImageResponse - 有

您可以使用:

for await (let resolvedPromise of arrayOfPromises) {
  console.log(resolvedPromise)
}

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of

如果您想使用 Promise.all(),您可以选择 Promise.allSettled() 所以你可以更好地控制被拒绝的承诺。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

使用modern-async's map()的解决方案:

import { map } from 'modern-async'

...
const result = await map(myArray, async (v) => {
    ...
})

使用该库的优点是您可以使用 mapLimit() or mapSeries().

控制并发性

这可能会对某人有所帮助。

const APISimulator = (v) => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve({ data: v });
    }, v * 100);
});

const arr = [7, 6, 5, 1, 2, 3];

const res = () => arr.reduce(async (memo, v, i) => {
    const results = await memo;
    console.log(`proccessing item-${i + 1} :`, v)
    await APISimulator(v);
    console.log(`completed proccessing-${i + 1} :`, v)

    return [...results, v];
}, []);

res().then(proccessed => console.log(proccessed))

这是最简单的方法。

await Promise.all(
    arr.map(async (element) => {
        ....
    })
)