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


当前回答

使用ES2018,您可以大大简化以上所有答案:

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

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

参见规范:建议异步迭代

简化:

  for await (const results of array) {
    await longRunningTask()
  }
  console.log('I will wait')

2018-09-10:这个答案最近受到了很多关注,有关异步迭代的更多信息,请参阅AxelRauschmayer的博客文章。

其他回答

使用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

若要查看如何出错,请在方法末尾打印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}`);
})();

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);
  });
})();

当然,代码确实有效,但我很确定它并没有达到您期望的效果。它只是触发多个异步调用,但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)
  }));
}

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

我想解决在异步和等待时使用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。。的,它们按顺序保存。