使用async await与Array.map一起使用

531

给定以下代码:

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

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

产生以下错误:

TS2322:类型“Promise<number>[]”不能赋值给类型“number[]”。类型“Promise<number>”不能赋值给类型“number”。

我该怎么修复它? 如何让 async awaitArray.map 协同工作?


10
你为什么想将同步操作变为异步操作?arr.map()是同步的,不会返回一个promise。 - jfriend00
4
你不能将一个异步操作发送给一个期望同步操作的函数,比如 map,并期望它能正常工作。 - Heretic Monkey
5
@jfriend00,我在内部函数中有许多等待语句。实际上,这是一个很长的函数,我只是为了使它可读而简化了它。现在我添加了一个等待调用,以使为什么它应该是异步更清晰。 - Alon
2
你需要等待返回一个Promise的东西,而不是返回一个数组的东西。 - jfriend00
async/await 不是 ES7 的一部分。 - Felix Kling
5
一个有用的事情要意识到的是,每当你将一个函数标记为“async”时,你就让这个函数返回一个promise。所以当然,一个异步映射会返回一个promise数组 :) - Anthony Manning-Franklin
10个回答

1099

这里的问题是你尝试使用await等待一个承诺数组而不是一个 Promise。这不会达到你的预期。

当传递给await的对象不是 Promise 时,await会立即将该值原样返回,而不是尝试解决它。因此,由于你在这里传递了一个数组(Promise 对象的数组)而不是 Promise,await返回的值仅仅是该数组,其类型为Promise<number>[]

你可能想要做的是在调用map返回的数组上调用Promise.all以将其转换为单个 Promise,然后再使用await

根据 MDN Promise.all 文档

Promise.all(iterable) 方法返回一个 Promise,当可迭代参数中所有的 promise 都被解决时,该 Promise 解决,或者在第一个拒绝的 promise 失败时,以失败原因拒绝。

所以在你的情况下:

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.anyPromise.race,而不是Promise.all,但在大多数情况下(几乎肯定包括此情况),Promise.all将是您想要的。

5
:冒号代表什么意思? - Daniel Node.js
29
@DanielPendergast 这是用于 TypeScript 中的类型注释。 - Ajedi32
在异步映射函数内部调用callAsynchronousOperation(item);时,使用和不使用await有什么区别? - nerdizzle
2
@nerdizzle 这听起来像是另一个问题的好候选人。基本上,使用 await 关键字,函数将等待异步操作完成(或失败)后再继续执行,否则它将立即继续执行而不会等待。 - Ajedi32

74

这是最简单的方法。

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

简单而实用。+1 - Movahhedi

60

以下是正确使用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(calc);
  const results = await Promise.all(unresolvedPromises);
  document.write(results);
};

document.write('calculating...');
asyncFunc();

另外还有codepen

请注意,我们只在Promise.all上使用“await”。我们多次调用calc而不带“await”,并立即收集未解决的promise数组。然后,Promise.all等待所有promise都解决并按顺序返回解决的值数组。


4
多年来我一直在使用Javascript时,总是遇到在数组上进行映射时因js的异步性质而破坏顺序的问题。我曾经使用过Promise.all,但从未想过它以现有形式解决了这个问题。这个答案彻底改变了我的编写方式,包括映射和其他方面。上帝保佑。 - sakib11
2
谢谢您提供的解决方案,我发现它比其他提供的方案更优雅和易读。 - S..

23

如果您没有使用原生的Promise,而是使用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;
  });

3
它与众不同 - 它不会并行运行所有操作,而是按顺序执行它们。 - Andrey Tserkus
5
Promise.mapSeriesPromise.each 是顺序执行的,而 Promise.map 则是同时开始执行所有任务。 - Kiechlus
1
@AndreyTserkus,您可以通过提供“并发”选项来并行运行所有或部分操作。 - user659682
40
值得一提的是,这不是纯粹的JavaScript。 - Michal
我在第一次阅读这个回答时没有注意到你在谈论Bluebird。如果能更新你的回答代码,明确地从Bluebird中导入Promise将会很好。 - SunnyPro

16

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


14

5

我建议使用如上所述的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的评论),请在let i in arr语句内使用of而不是in


14
Promise.all会对数组中的每个元素进行异步操作。这是同步的,必须等待一个元素完成后才能开始下一个元素。 - Santiago Mendoza Ramirez
4
对于那些尝试这种方法的人,请注意,for..of是迭代数组内容的正确方式,而for..in则迭代索引。 - ralfoide

4

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

import { map } from 'modern-async'

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

使用该库的优势在于您可以使用mapLimit()mapSeries()来控制并发。

0
顺便提一下,注意你可以轻松地创建一个扩展来使其更加简洁,并像这样使用它:
var arr = [1, 2, 3, 4, 5];

var results = await arr.mapAsync(async (e)  => {
    return await callAsynchronousOperation(e);
});

你只需要创建一个名为extensions.ts的文件。
export {};

declare global {
  interface Array<T> {
    mapAsync<U>(mapFn: (value: T, index: number, array: T[]) => Promise<U>): Promise<U[]>;
}

if (!Array.prototype.mapAsync) {
  Array.prototype.mapAsync = async function <U, T>(
    mapFn: (value: T, index: number, array: T[]) => Promise<U>
  ): Promise<U[]> {
    return await Promise.all(
      (this as T[]).map(async (v, i, a): Promise<U> => {
        return await mapFn(v, i, a);
      })
    );
  };
}

0

我在后端任务中需要从存储库中查找所有实体,并添加一个新的属性url并返回到控制器层。这是我如何实现的(感谢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 - 有


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接