在forEach循环中使用async/await有什么问题吗?我正在尝试循环浏览一系列文件,并等待每个文件的内容。

import fs from 'fs-promise'

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
}

printFiles()

这段代码确实有效,但这段代码会出错吗?我有人告诉我,你不应该在这样的高阶函数中使用async/await,所以我只想问问这是否有问题。


当前回答

除了@Bergi的回答,我想提供第三种选择。这与@Bergi的第二个例子非常相似,但不是单独等待每个readFile,而是创建一个承诺数组,每个承诺都在最后等待。

import fs from 'fs-promise';
async function printFiles () {
  const files = await getFilePaths();

  const promises = files.map((file) => fs.readFile(file, 'utf8'))

  const contents = await Promise.all(promises)

  contents.forEach(console.log);
}

注意,传递给.map()的函数不需要是异步的,因为fs.readFile无论如何都会返回Promise对象。因此,Promise是Promise对象的数组,可以将其发送到Promise.all()。

在@Bergi的回答中,控制台可以按照文件内容的读取顺序记录文件内容。例如,如果一个非常小的文件在一个非常大的文件之前完成了读取,那么它将首先被记录,即使小文件在文件数组中位于大文件之后。然而,在我上面的方法中,您可以保证控制台将以与提供的阵列相同的顺序记录文件。

其他回答

此解决方案还对内存进行了优化,因此您可以在10000个数据项和请求上运行它。这里的一些其他解决方案将使大型数据集上的服务器崩溃。

在TypeScript中:

export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
        for (let index = 0; index < array.length; index++) {
            await callback(array[index], index);
        }
    }

如何使用?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})

@贝吉已经给出了如何正确处理这一特殊案件的答案。我不会在这里重复。

我想解决在异步和等待时使用forEach和for循环之间的区别

forEach的工作原理

让我们看看forEach是如何工作的。根据ECMAScript规范,MDN提供了一种可以用作polyfill的实现。我将其复制并粘贴到此处,并删除注释。

Array.prototype.forEach = function (callback, thisArg) {
  if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); }
  var T, k;
  var O = Object(this);
  var len = O.length >>> 0;
  if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); }
  if (arguments.length > 1) { T = thisArg; }
  k = 0;
  while (k < len) {
    var kValue;
    if (k in O) {
      kValue = O[k];
      callback.call(T, kValue, k, O); // pay attention to this line
    }
    k++;
  }
};

让我们回到代码,将回调作为函数提取。

async function callback(file){
  const contents = await fs.readFile(file, 'utf8')
  console.log(contents)
}

所以,回调基本上返回一个promise,因为它是用异步声明的。在forEach内部,回调只是以正常方式调用,如果回调本身返回一个promise,javascript引擎不会等待它被解析或拒绝。相反,它将承诺放入作业队列中,并继续执行循环。

如何在回调中等待fs.readFile(文件,'utf8')?

基本上,当异步回调有机会被执行时,js引擎将暂停,直到fs.readFile(文件,'utf8')被解析或拒绝,并在完成后继续执行异步函数。因此contents变量存储fs.readFile的实际结果,而不是promise。因此,console.log(contents)注销文件内容,而不是Promise

为什么。。。作品?

当我们编写循环的泛型for时,我们获得了比forEach更多的控制权。让我们重构printFiles。

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
    // or await callback(file)
  }
}

当为循环求值时,我们在异步函数中有await promise,执行将暂停,直到await promice得到解决。因此,您可以认为文件是按确定的顺序逐个读取的。

按顺序执行

有时,我们确实需要以顺序执行异步函数。例如,我有几个新记录存储在一个数组中,要保存到数据库中,我希望它们按顺序保存,这意味着数组中的第一个记录应该先保存,然后再保存,直到保存最后一个记录。

下面是一个示例:

常量记录=[1,2,3,4];异步函数saveRecord(record){return new Promise((已解决,已拒绝)=>{setTimeout(()=>{已解析(`record${record}已保存`)},数学随机(*500)});}EachSaveRecords(记录)的异步函数{records.forEach(异步(记录)=>{const res=等待saveRecord(记录);console.log(res);})}SaveRecords(记录)的异步函数{for(记录的常量记录){const res=等待saveRecord(记录);console.log(res);}}(异步()=>{console.log(“===保存记录的===”)等待保存记录(记录)console.log(“==对于每个保存记录==”)等待EachSaveRecords(记录)})()

我使用setTimeout来模拟将记录保存到数据库的过程——这是异步的,花费了随机时间。使用forEach,记录将按未确定的顺序保存,但使用for。。的,它们按顺序保存。

files.forEach(异步(文件)=>{const contents=await fs.readFile(文件,'utf8')})

问题是,forEach()忽略了迭代函数返回的promise。在每个异步代码执行完成后,forEach不会等待移动到下一个迭代。所有fs.readFile函数将在同一轮事件循环中调用,这意味着它们是并行启动的,而不是顺序启动的,并且在调用forEach()后立即继续执行等待所有fs.readFile操作完成。由于forEach不等待每个promise解析,因此循环实际上在解析promise之前完成了迭代。您希望在forEach完成后,所有异步代码都已执行,但事实并非如此。您最终可能会尝试访问尚未可用的值。

您可以使用以下示例代码测试行为

常量数组=[1,2,3];const sleep=(ms)=>new Promise(解析=>setTimeout(解析,ms));常量delayedSquare=(num)=>睡眠(100)。然后(()=>num*num);const testForEach=(numbersArray)=>{常量存储=[];//此处将此代码视为同步代码numbersArray.forEach(异步(num)=>{const squaredNum=等待延迟平方(num);//这将控制相应的squaredNum值console.log(平方数);store.push(平方数);});//您希望存储阵列已填充,但未填充//这将返回[]console.log(“存储”,存储);};testForEach(数组);//注意,当您测试时,将记录第一个“store[]”//然后squaredNum的内部forEach将记录

解决方案是使用for of循环。

for (const file of files){
    const contents = await fs.readFile(file, 'utf8')
}

当然,代码确实有效,但我很确定它并没有达到您期望的效果。它只是触发多个异步调用,但printFiles函数在这之后会立即返回。

按顺序读取

如果您想按顺序读取文件,则实际上不能使用forEach。只需使用现代for…of循环,其中await将按预期工作:

async function printFiles () {
  const files = await getFilePaths();

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }
}

并行读取

如果要并行读取文件,则不能使用forEach。每一个异步回调函数调用都会返回一个promise,但您要丢弃它们而不是等待它们。只需使用map,您就可以等待Promise.all提供的一系列承诺:

async function printFiles () {
  const files = await getFilePaths();

  await Promise.all(files.map(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  }));
}

除了@Bergi的回答,我想提供第三种选择。这与@Bergi的第二个例子非常相似,但不是单独等待每个readFile,而是创建一个承诺数组,每个承诺都在最后等待。

import fs from 'fs-promise';
async function printFiles () {
  const files = await getFilePaths();

  const promises = files.map((file) => fs.readFile(file, 'utf8'))

  const contents = await Promise.all(promises)

  contents.forEach(console.log);
}

注意,传递给.map()的函数不需要是异步的,因为fs.readFile无论如何都会返回Promise对象。因此,Promise是Promise对象的数组,可以将其发送到Promise.all()。

在@Bergi的回答中,控制台可以按照文件内容的读取顺序记录文件内容。例如,如果一个非常小的文件在一个非常大的文件之前完成了读取,那么它将首先被记录,即使小文件在文件数组中位于大文件之后。然而,在我上面的方法中,您可以保证控制台将以与提供的阵列相同的顺序记录文件。