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


当前回答

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

其他回答

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

OP的原始问题

在forEach循环中使用async/await有什么问题吗。。。

在@Bergi选择的答案中,它展示了如何串行和并行处理。然而,并行性还存在其他问题-

订单--@chharvey注意到-

例如,如果一个非常小的文件在一个非常大的文件之前完成了读取,那么它将首先被记录,即使小文件在文件数组中位于大文件之后。

可能一次打开太多文件--Bergi在另一个答案下的评论

同时打开数千个文件以同时读取它们也是不好的。人们总是要评估顺序、并行或混合方法是否更好。

因此,让我们来解决这些问题,展示实际的代码,简洁明了,不使用第三方库。易于剪切、粘贴和修改的东西。

并行读取(一次读取),串行打印(每个文件尽可能早)。

最简单的改进是像@Bergi的回答那样执行完全并行,但做了一个小改动,以便在保持顺序的同时尽快打印每个文件。

async function printFiles2() {
  const readProms = (await getFilePaths()).map((file) =>
    fs.readFile(file, "utf8")
  );
  await Promise.all([
    await Promise.all(readProms),                      // branch 1
    (async () => {                                     // branch 2
      for (const p of readProms) console.log(await p);
    })(),
  ]);
}

上面,两个单独的分支同时运行。

分支1:同时并行读取,分支2:连续读取以强制排序,但等待时间不超过必要

这很容易。

在并发限制下并行读取,串行打印(每个文件尽可能早)。

“并发限制”意味着同时读取的文件不超过N个。就像一家一次只允许这么多顾客进入的商店(至少在新冠疫情期间)。

首先引入了一个helper函数-

function bootablePromise(kickMe: () => Promise<any>) {
  let resolve: (value: unknown) => void = () => {};
  const promise = new Promise((res) => { resolve = res; });
  const boot = () => { resolve(kickMe()); };
  return { promise, boot };
}

函数bootablePromise(kickMe:()=>Promise<any>)需要函数kickMe作为启动任务的参数(在本例中为readFile),但不会立即启动。

bootablePromise返回几个财产

承诺类型承诺引导类型函数()=>void

承诺有两个阶段

承诺开始一项任务作为一个承诺,完成一项已经开始的任务。

当调用boot()时,promise从第一状态转换到第二状态。

bootablePromise用于printFiles--

async function printFiles4() {
  const files = await getFilePaths();
  const boots: (() => void)[] = [];
  const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
  const bootableProms = files.map((file,pidx) => {
    const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
    boots.push(boot);
    set.add(promise.then(() => ({ pidx })));
    return promise;
  });
  const concurLimit = 2;
  await Promise.all([
    (async () => {                                       // branch 1
      let idx = 0;
      boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
      while (idx<boots.length) {
        const { pidx } = await Promise.race([...set]);
        set.delete([...set][pidx]);
        boots[idx++]();
      }
    })(),
    (async () => {                                       // branch 2
      for (const p of bootableProms) console.log(await p);
    })(),
  ]);
}

和以前一样,有两个分支

分支1:用于运行和处理并发。分支2:用于打印

现在的区别是不允许并发运行超过concurrentLimit Promise。

重要的变量是

boots:要调用以强制其相应Promise转换的函数数组。它仅在分支1中使用。set:在随机访问容器中有Promise,这样一旦实现,就可以很容易地删除它们。此容器仅在分支1中使用。bootableProms:这些是与最初在集合中的Promise相同的Promise,但它是一个数组而不是集合,并且该数组从未更改。它仅在分支2中使用。

使用模拟fs.readFile运行,所需时间如下(文件名与时间(毫秒))。

const timeTable = {
  "1": 600,
  "2": 500,
  "3": 400,
  "4": 300,
  "5": 200,
  "6": 100,
};

可以看到这样的测试运行时间,显示并发正在运行--

[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005

可在typescript游乐场沙盒中执行

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

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

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

这里是一个在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
   }
)