在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,所以我只想问问这是否有问题。


当前回答

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')
}

其他回答

若要查看如何出错,请在方法末尾打印console.log。

一般情况下可能出错的事情:

任意顺序。printFiles可以在打印文件之前完成运行。性能差。

这些并不总是错误的,但通常在标准用例中。

通常,使用forEach将导致除最后一个之外的所有结果。它将在不等待函数的情况下调用每个函数,这意味着它将告诉所有函数开始,然后完成,而不等待函数完成。

import fs from 'fs-promise'

async function printFiles () {
  const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'))

  for(const file of files)
    console.log(await file)
}

printFiles()

这是本机JS中的一个示例,它将保持顺序,防止函数过早返回,并在理论上保持最佳性能。

这将:

启动所有并行文件读取。通过使用映射将文件名映射到要等待的承诺来保持顺序。按照数组定义的顺序等待每个承诺。

使用此解决方案,第一个文件将在其可用时立即显示,而无需等待其他文件首先可用。

它还将同时加载所有文件,而不必等待第一个文件完成后才能开始第二次文件读取。

这和原始版本的唯一缺点是,如果一次启动多个读取,则由于一次可能发生更多错误,因此处理错误更困难。

对于一次读取一个文件的版本,则会在出现故障时停止,而不会浪费时间尝试读取更多文件。即使有一个精心设计的取消系统,也很难避免它在第一个文件上失败,但也很难读取大部分其他文件。

性能并不总是可预测的。虽然许多系统的并行文件读取速度会更快,但有些系统更倾向于顺序读取。有些是动态的,可能会在负载下发生变化,提供延迟的优化在激烈竞争下并不总能产生良好的吞吐量。

该示例中也没有错误处理。如果有什么东西要求他们要么全部成功展示,要么根本不展示,那它就做不到。

建议在每个阶段使用console.log进行深入实验,并使用假文件读取解决方案(随机延迟)。尽管许多解决方案在简单的情况下似乎都是一样的,但它们都有细微的差异,需要额外的仔细检查才能挤出。

使用此模拟来帮助区分解决方案之间的差异:

(async () => {
  const start = +new Date();
  const mock = () => {
    return {
      fs: {readFile: file => new Promise((resolve, reject) => {
        // Instead of this just make three files and try each timing arrangement.
        // IE, all same, [100, 200, 300], [300, 200, 100], [100, 300, 200], etc.
        const time = Math.round(100 + Math.random() * 4900);
        console.log(`Read of ${file} started at ${new Date() - start} and will take ${time}ms.`)
        setTimeout(() => {
          // Bonus material here if random reject instead.
          console.log(`Read of ${file} finished, resolving promise at ${new Date() - start}.`);
          resolve(file);
        }, time);
      })},
      console: {log: file => console.log(`Console Log of ${file} finished at ${new Date() - start}.`)},
      getFilePaths: () => ['A', 'B', 'C', 'D', 'E']
    };
  };

  const printFiles = (({fs, console, getFilePaths}) => {
    return async function() {
      const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'));

      for(const file of files)
        console.log(await file);
    };
  })(mock());

  console.log(`Running at ${new Date() - start}`);
  await printFiles();
  console.log(`Finished running at ${new Date() - start}`);
})();

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

您可以使用async包中的async.forEach循环:

async.forEach(dataToLoop(array), async(data, cb) => {
                variable = await MongoQuery;
            }, function(err) {
                console.log(err);  
              })
            })
            .catch((err)=>{
              console.log(err);
            })

当fs基于承诺时,Bergi的解决方案非常有效。您可以使用bluebird、fs extra或fs promise。

然而,节点的本地fs库的解决方案如下:

const result = await Promise.all(filePaths
    .map( async filePath => {
      const fileContents = await getAssetFromCache(filePath, async function() {

        // 1. Wrap with Promise    
        // 2. Return the result of the Promise
        return await new Promise((res, rej) => {
          fs.readFile(filePath, 'utf8', function(err, data) {
            if (data) {
              res(data);
            }
          });
        });
      });

      return fileContents;
    }));

注:require('fs')强制将函数作为第三个参数,否则抛出错误:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

npm上的p-iteration模块实现了Array迭代方法,因此它们可以以非常简单的方式与async/await一起使用。

您的案例示例:

const { forEach } = require('p-iteration');
const fs = require('fs-promise');

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

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