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


当前回答

我使用Array.prototype.reduce代替Promise.all和Array.prototy.map(这不能保证Promise的解析顺序),从解析的Promise开始:

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

  await files.reduce(async (promise, file) => {
    // This line will wait for the last async function to finish.
    // The first iteration uses an already resolved Promise
    // so, it will immediately continue.
    await promise;
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }, Promise.resolve());
}

其他回答

当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

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

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

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游乐场沙盒中执行

图片值1000字-仅用于顺序方法


背景:昨晚我也遇到了类似的情况。我使用异步函数作为foreach参数。结果是不可预测的。当我对代码进行了3次测试时,它运行了2次没有问题,1次失败。(有些奇怪)

最后我改变了主意,做了一些擦试板测试。

场景1-foreach中的异步如何实现不连续

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  myPromiseArray.forEach(async (element, index) => {
    let result = await element;
    console.log(result);
  })

  console.log('After For Each Loop')
}

main();

场景2-使用上面@Bergi建议的for-of循环

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well
  for (const element of myPromiseArray) {
    let result = await element;
    console.log(result)
  }

  console.log('After For Each Loop')
}

main();

如果你是像我这样的小老派,你可以简单地使用经典for循环,这也很有效:)

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well too - the classic for loop :)
  for (let i = 0; i < myPromiseArray.length; i++) {
    const result = await myPromiseArray[i];
    console.log(result);
  }

  console.log('After For Each Loop')
}

main();

我希望这对某人有所帮助,祝你愉快!