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


当前回答

这里是一个在forEach循环中使用异步的好例子。

编写自己的asyncForEach

async function asyncForEach(array, callback) {  
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array)
    }
}

你可以这样用

await asyncForEach(array, async function(item,index,array){
     //await here
   }
)

其他回答

使用Task、futurize和可遍历列表,您可以简单地

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

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

这是你如何设置的

import fs from 'fs';
import { futurize } from 'futurize';
import Task from 'data.task';
import { List } from 'immutable-ext';

const future = futurizeP(Task)
const readFile = future(fs.readFile)

构建所需代码的另一种方法是

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

或者甚至更注重功能

// 90% of encodings are utf-8, making that use case super easy is prudent

// handy-library.js
export const readFile = f =>
  future(fs.readFile)( f, 'utf-8' )

export const arrayToTaskList = list => taskFn => 
  List(files).traverse( Task.of, taskFn ) 

export const readFiles = files =>
  arrayToTaskList( files, readFile )

export const printFiles = files => 
  readFiles(files).fork( console.error, console.log)

然后从父函数

async function main() {
  /* awesome code with side-effects before */
  printFiles( await getFiles() );
  /* awesome code with side-effects after */
}

如果你真的想在编码上有更多的灵活性,你可以这样做(为了好玩,我使用了建议的Pipe Forward操作符)

import { curry, flip } from 'ramda'

export const readFile = fs.readFile 
  |> future,
  |> curry,
  |> flip

export const readFileUtf8 = readFile('utf-8')

PS-我没有在控制台上尝试这段代码,可能有一些拼写错误。。。正如90年代的孩子们所说的那样,“直式自由泳,从穹顶上跳下来!”-p

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

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

如果要同时迭代所有元素:

async function asyncForEach(arr, fn) {
  await Promise.all(arr.map(fn));
}

如果您希望非并发地遍历所有元素(例如,当映射函数具有副作用或同时在所有数组元素上运行mapper时,资源成本太高):

选项A:承诺

function asyncForEachStrict(arr, fn) {
  return new Promise((resolve) => {
    arr.reduce(
      (promise, cur, idx) => promise
        .then(() => fn(cur, idx, arr)),
      Promise.resolve(),
    ).then(() => resolve());
  });
}

选项B:异步/等待

async function asyncForEachStrict(arr, fn) {
  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    await fn(cur, idx, arr);
  }
}

正如其他答案所提到的,您可能希望它按顺序而不是并行执行。即,运行第一个文件,等待完成,然后一旦完成,运行第二个文件。这不会发生。

我认为很重要的是要解决为什么没有发生这种情况。

想想forEach是如何工作的。我找不到来源,但我认为它的工作原理如下:

const forEach = (arr, cb) => {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i]);
  }
};

现在想想当你做这样的事情时会发生什么:

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

在forEach的for循环中,我们调用cb(arr[i]),最后是logFile(file)。logFile函数内部有一个await,所以for循环可能会在继续到i++之前等待这个await?

不,不会的。令人困惑的是,这不是wait的工作方式。从文档中:

await分割执行流,允许异步函数的调用方继续执行。在await延迟异步函数的继续之后,随后执行后续语句。如果此await是其函数执行的最后一个表达式,则继续执行,方法是向函数的调用方返回完成await函数的未决Promise并继续执行该调用方。

因此,如果您有以下内容,则不会在“b”之前记录数字:

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

const logNumbers = async () => {
  console.log(1);
  await delay(2000);
  console.log(2);
  await delay(2000);
  console.log(3);
};

const main = () => {
  console.log("a");
  logNumbers();
  console.log("b");
};

main();

循环回到forEach,forEach就像main,logFile就像logNumbers。main不会因为logNumbers等待而停止,forEach不会因为logFile等待而停止。

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