ChatGPT解决这个技术问题 Extra ChatGPT

将 async/await 与 forEach 循环一起使用

forEach 循环中使用 async/await 是否有任何问题?我正在尝试遍历文件数组并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,所以我只是想问一下这是否有任何问题。

@KernelMode forEach 方法是这里的高阶函数

m
mesqueeb

当然代码确实可以工作,但我很确定它没有按照您的预期做。它只是触发多个异步调用,但 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。每个 async 回调函数调用都返回一个 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)
  }));
}

您能否解释一下为什么 for ... of ... 有效?
好的,我知道为什么...使用 Babel 会将 async/await 转换为生成器函数,使用 forEach 意味着每次迭代都有一个单独的生成器函数,与其他迭代无关。因此它们将独立执行,并且与其他人没有 next() 的上下文。实际上,一个简单的 for() 循环也可以工作,因为迭代也在一个生成器函数中。
@Demonbane:简而言之,因为它旨在工作:-) await 暂停当前的 function 评估,包括所有控制结构。是的,在这方面它与生成器非常相似(这就是它们用于填充 async/await 的原因)。
@arve0 并非如此,async 函数与 Promise 执行器回调完全不同,但是 map 回调在这两种情况下都会返回一个承诺。
@Taurus 如果您不打算等待它们,那么 for…of 将与 forEach 一样工作。不,我的意思是要强调现代 JS 代码中没有 .forEach 的位置。
S
Steffan

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

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

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

参见规范:proposal-async-iteration

简化:

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

2018-09-10:这个答案最近引起了很多关注,有关异步迭代的更多信息,请参阅Axel Rauschmayer's blog post


我认为这个答案不能解决最初的问题。 for-await-of 具有同步迭代(在我们的例子中是一个数组)不包括在每次迭代中使用异步操作同时迭代一个数组的情况。如果我没记错的话,将 for-await-of 与非承诺值的同步迭代一起使用与使用普通的 for-of 相同。
我们如何在这里将 files 数组委托给 fs.readFile?它来自可迭代?
使用这个解决方案,每次迭代都会等待前一个迭代,如果操作正在进行一些长时间的计算或读取一个长文件,它将阻止下一次的执行,而不是将所有函数映射到 Promise 并等待它们完成.
此答案与 OP 具有相同的问题:它并行访问所有文件。结果的序列化打印只是隐藏了它。
这个答案是错误的。 files.map() 返回一个 Promise 数组,不是异步迭代器for await 就是为此而生的! It will cause unhandled-rejection crashes
T
Timothy Zorn

我没有将 Promise.allArray.prototype.map 结合使用(它不能保证 Promise 的解析顺序),而是使用 Array.prototype.reduce,从已解析的 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());
}

这非常有效,非常感谢。您能解释一下 Promise.resolve()await promise; 发生了什么吗?
这很酷。我是否认为文件将按顺序读取,而不是一次全部读取?
@parrker9 Promise.resolve() 返回一个已解析的 Promise 对象,因此 reduce 有一个 Promise 开始。 await promise; 将等待链中的最后一个 Promise 解决。 @GollyJer 文件将按顺序处理,一次一个。
@Shay,您的意思是顺序的,而不是同步的。这仍然是异步的 - 如果安排了其他事情,它们将在这里的迭代之间运行。
如果您需要尽可能快地完成异步进程并且您不关心它们是否按顺序完成,请尝试使用 Promise.all 获得大量支持的所提供解决方案之一。示例:Promise.all(files.map(async (file) => { /* code */ }));
A
Antonio Val

npm 上的 p-iteration 模块实现了数组迭代方法,因此可以非常直接地与 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);
  });
})();

m
mikemaccana

以下是一些 forEachAsync 原型。请注意,您需要 await 它们:

Array.prototype.forEachAsync = async function (fn) {
    for (let t of this) { await fn(t) }
}

Array.prototype.forEachAsyncParallel = async function (fn) {
    await Promise.all(this.map(fn));
}

请注意,虽然您可以将其包含在您自己的代码中,但您不应将其包含在您分发给其他人的库中(以避免污染他们的全局变量)。


用法:等待 myArray。 forEachAsyncParallel( async (item) => { await myAsyncFunction(item) })
@Matt,如果它不是异步的,等待 fn 不是问题吗?如果给定的输入是同步函数怎么办? stackoverflow.com/a/53113299/18387350
k
krupesh Anadkat

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

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

最后我明白了,做了一些便笺簿测试。

场景 1 - 在 foreach 中使用 async 可以获得多么不连续

https://i.stack.imgur.com/VGXEa.png

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 - 使用 for - of 循环,如上面建议的@Bergi

https://i.stack.imgur.com/QWbRI.png

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

我希望这对某人有帮助,美好的一天,干杯!


如果有人想知道 vscode 主题是什么 - 它是 github 的官方轻主题。 & 如果有人用如此明亮的快照伤害了他们的眼睛,我很抱歉😅
我建议使用短语“Before/After Loop”在不是“For Each Loop”时会减少混乱。
兄弟在这里只是像一个绝对的异教徒一样使用 Githubs 官方编写代码。我什至没有生气。各有各的。尽管如此,我会缓存 length 以加快 for 循环并防止在每次迭代之间重新计算。
s
sam

@Bergi 已经给出了如何正确处理这种特殊情况的答案。我不会在这里复制。

在涉及 asyncawait 时,我想解决使用 forEachfor 循环之间的区别

forEach 的工作原理

让我们看看 forEach 是如何工作的。根据 ECMAScript Specification,MDN 提供了一个可以用作 polyfill 的 implementation。我将其复制并粘贴到此处并删除评论。

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

因此,基本上 callback 返回一个承诺,因为它是用 async 声明的。在 forEach 内部,callback 只是以正常方式调用,如果回调本身返回一个 Promise,则 javascript 引擎不会等待它被解析或拒绝。相反,它将 promise 放入作业队列,并继续执行循环。

callback 中的 await fs.readFile(file, 'utf8') 怎么样?

基本上,当您的 async callback 有机会执行时,js 引擎将暂停直到 fs.readFile(file, 'utf8') 被解析或拒绝,并在完成后恢复执行 async 函数。因此,contents 变量存储来自 fs.readFile 的实际结果,而不是 promise。因此,console.log(contents) 注销文件内容而不是 Promise

为什么 for ... of 有效?

当我们编写一个通用的 for of 循环时,我们获得了比 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)
  }
}

当评估 for 循环时,我们在 async 函数中有 await 承诺,执行将暂停,直到 await 承诺完成。因此,您可以认为文件是按照确定的顺序逐一读取的。

依次执行

有时,我们确实需要按顺序执行异步函数。例如,我有一些新记录存储在要保存到数据库的数组中,我希望它们按顺序保存,这意味着数组中的第一条记录应该首先保存,然后是第二条,直到最后一条记录被保存。

这是一个例子:

常量记录 = [1, 2, 3, 4];异步函数 saveRecord(record) { return new Promise((resolved, denied) => { setTimeout(()=> { resolved(`record ${record} saved`) }, Math.random() * 500) }); } async function forEachSaveRecords(records) { records.forEach(async (record) => { const res = await saveRecord(record); console.log(res); }) } async function forofSaveRecords(records) { for (const record of记录) { const res = 等待 saveRecord(record);控制台.log(res); } } (async () => { console.log("=== for of save records ===") await forofSaveRecords(records) console.log("=== forEach save records ===") await forEachSaveRecords(记录) })()

我使用 setTimeout 来模拟将记录保存到数据库的过程 - 它是异步的并且花费随机时间。使用 forEach,记录以不确定的顺序保存,但使用 for..of,它们按顺序保存。


简而言之:foreach 不以异步方式处理回调,因此无需等待。
m
mudamudamuda

该解决方案还针对内存进行了优化,因此您可以在 10,000 个数据项和请求上运行它。这里的一些其他解决方案会使服务器在大型数据集上崩溃。

在打字稿中:

export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
        for (let index = 0; index < array.length; index++) {
            await callback(array[index], index);
        }
    }

如何使用?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})

我认为如果您可以在如何使用部分完成此示例 :) 将会很有帮助。对于我的情况: await asyncForEach(configuration.groupNames, async (groupName) => { await AddUsersToGroup(configuration, groupName); })
谢谢,很好的解决方案!!
Y
Yilmaz

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

问题是,迭代函数返回的承诺被 forEach() 忽略。因此,所有 fs.readFile 函数都在同一轮事件循环中调用,这意味着它们是并行启动的,而不是顺序启动的,并且在调用 forEach() 后立即继续执行,而无需等待所有 { 2}操作完成。由于 forEach 不等待每个 Promise 解决,因此循环实际上在 Promise 解决之前完成迭代。您最终可能会尝试访问尚不可用的值。


c
chharvey

除了 @Bergi’s answer,我想提供第三种选择。它与@Bergi 的第二个示例非常相似,但不是单独等待每个 readFile ,而是创建一个 promise 数组,每个都在最后等待。

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() 的函数不必是 async,因为 fs.readFile 无论如何都会返回一个 Promise 对象。因此 promises 是一个 Promise 对象数组,可以发送到 Promise.all()

在@Bergi 的回答中,控制台可能会按照读取的顺序记录文件内容。例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使小文件在 files 数组中的大文件之后 也是如此。但是,在我上面的方法中,您可以保证控制台将以与提供的数组相同的顺序记录文件。


y
yeah22

替换不起作用的 forEach() 等待循环的简单直接解决方案是将 forEach 替换为 map 并将 Promise.all( 添加到开头。

例如:

await y.forEach(async (x) => {

await Promise.all(y.map(async (x) => {

最后需要一个额外的 )


不太一样。 Promise.all 将同时运行所有的 Promise。 for 循环意味着是顺序的。
J
Jay Edwards

在一个文件中弹出几个方法非常容易,这些方法将按序列化顺序处理异步数据并为您的代码提供更传统的风格。例如:

module.exports = function () {
  var self = this;

  this.each = async (items, fn) => {
    if (items && items.length) {
      await Promise.all(
        items.map(async (item) => {
          await fn(item);
        }));
    }
  };

  this.reduce = async (items, fn, initialValue) => {
    await self.each(
      items, async (item) => {
        initialValue = await fn(initialValue, item);
      });
    return initialValue;
  };
};

现在,假设它保存在“./myAsync.js”中,您可以在相邻文件中执行类似于以下内容的操作:

...
/* your server setup here */
...
var MyAsync = require('./myAsync');
var Cat = require('./models/Cat');
var Doje = require('./models/Doje');
var example = async () => {
  var myAsync = new MyAsync();
  var doje = await Doje.findOne({ name: 'Doje', noises: [] }).save();
  var cleanParams = [];

  // FOR EACH EXAMPLE
  await myAsync.each(['bork', 'concern', 'heck'], 
    async (elem) => {
      if (elem !== 'heck') {
        await doje.update({ $push: { 'noises': elem }});
      }
    });

  var cat = await Cat.findOne({ name: 'Nyan' });

  // REDUCE EXAMPLE
  var friendsOfNyanCat = await myAsync.reduce(cat.friends,
    async (catArray, friendId) => {
      var friend = await Friend.findById(friendId);
      if (friend.name !== 'Long cat') {
        catArray.push(friend.name);
      }
    }, []);
  // Assuming Long Cat was a friend of Nyan Cat...
  assert(friendsOfNyanCat.length === (cat.friends.length - 1));
}

小附录,不要忘记将您的 await/asyncs 包装在 try/catch 块中!
m
master_dodo

fs 基于 Promise 时,Bergi's solution 工作得很好。为此,您可以使用 bluebirdfs-extrafs-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

J
Johnz

从循环中调用异步方法是不好的。这是因为每次循环迭代都会延迟到整个异步操作完成。这不是很高效。它还避免了 async/await 的并行化优势。

更好的解决方案是一次创建所有 Promise,然后使用 Promise.all() 访问结果。否则,每个后续操作将在前一个操作完成之前不会开始。

因此,代码可以重构如下;

const printFiles = async () => {
  const files = await getFilePaths();
  const results = [];
  files.forEach((file) => {
    results.push(fs.readFile(file, 'utf8'));
  });
  const contents = await Promise.all(results);
  console.log(contents);
}

一次打开数千个文件以同时读取它们也不好。人们总是需要评估顺序、并行或混合方法是否更好。顺序循环从根本上来说并不坏,await 实际上首先使它们成为可能。此外,它们并没有“避免异步执行的好处”,因为您仍然可以一次运行多个这样的循环(例如,对 printFiles 的两个并发调用)。
L
LeOn - Han Li

一个重要的警告是:await + for .. of 方法和 forEach + async 方式实际上具有不同的效果。

在真正的 for 循环中包含 await 将确保所有异步调用都被一一执行。而 forEach + async 方式会同时触发所有的 Promise,速度更快但有时会不堪重负(如果您执行一些数据库查询或访问一些有容量限制的 Web 服务并且不想触发一次 100,000 个呼叫)。

如果您不使用 async/await 并希望确保文件被一个接一个地读取,您也可以使用 reduce + promise(不太优雅)。

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

或者您可以创建一个 forEachAsync 来提供帮助,但基本上使用相同的 for 循环底层。

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}

看看How to define method in javascript on Array.prototype and Object.prototype so that it doesn't appear in for in loop。此外,您可能应该使用与本机 forEach 相同的迭代 - 访问索引而不是依赖可迭代性 - 并将索引传递给回调。
您可以通过使用异步函数的方式使用 Array.prototype.reduce。我在回答中展示了一个示例:stackoverflow.com/a/49499491/2537258
H
Hooman Askari

上述两种解决方案都有效,然而,Antonio 用更少的代码完成了这项工作,这就是它如何帮助我从我的数据库中解析数据,从几个不同的子 refs 中解析数据,然后将它们全部推入一个数组并在一个承诺中解决它毕竟是完毕:

Promise.all(PacksList.map((pack)=>{
    return fireBaseRef.child(pack.folderPath).once('value',(snap)=>{
        snap.forEach( childSnap => {
            const file = childSnap.val()
            file.id = childSnap.key;
            allItems.push( file )
        })
    })
})).then(()=>store.dispatch( actions.allMockupItems(allItems)))

g
gsaandy

只是添加到原来的答案

原始答案中的并行阅读语法有时令人困惑且难以阅读,也许我们可以用不同的方法编写它

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

  const readAndLogFile = async filePath => {
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
    return contents;
  };

  files.forEach(file => {
    fileReadPromises.push(readAndLogFile(file));
  });

  await Promise.all(fileReadPromises);
}

对于顺序操作,不只是 for...of,正常的 for 循环也可以工作

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

  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
  }
}


l
lukaswilkeer

就像@Bergi 的回应一样,但有一点不同。

如果一个被拒绝,Promise.all 会拒绝所有的承诺。

所以,使用递归。

const readFilesQueue = async (files, index = 0) {
    const contents = await fs.readFile(files[index], 'utf8')
    console.log(contents)

    return files.length <= index
        ? readFilesQueue(files, ++index)
        : files

}

const printFiles async = () => {
    const files = await getFilePaths();
    const printContents = await readFilesQueue(files)

    return printContents
}

printFiles()

附言

readFilesQueueprintFiles 之外导致 console.log 引入的副作用*,最好模拟、测试和/或窥探,所以有一个返回内容的函数(旁注)并不酷。

因此,代码可以简单地设计为:三个独立的“纯”函数**并且没有引入副作用,处理整个列表,并且可以轻松修改以处理失败的情况。

const files = await getFilesPath()

const printFile = async (file) => {
    const content = await fs.readFile(file, 'utf8')
    console.log(content)
}

const readFiles = async = (files, index = 0) => {
    await printFile(files[index])

    return files.lengh <= index
        ? readFiles(files, ++index)
        : files
}

readFiles(files)

未来编辑/当前状态

Node 支持顶级等待(它还没有插件,不会有并且可以通过和谐标志启用),它很酷但不能解决一个问题(从战略上讲,我只在 LTS 版本上工作)。如何获取文件?

使用组合。给定代码,让我感觉这是在模块内部,因此应该有一个函数来执行此操作。如果没有,您应该使用 IIFE 将角色代码包装到一个异步函数中,创建一个可以为您完成所有工作的简单模块,或者您可以采用正确的方式,即组合。

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

请注意,变量的名称会因语义而改变。您传递一个仿函数(一个可以被另一个函数调用的函数)并接收一个内存指针,该指针包含应用程序的初始逻辑块。

但是,如果不是模块,您需要导出逻辑吗?

将函数包装在异步函数中。

export const readFilesQueue = async () => {
    // ... to code goes here
}

或者更改变量的名称,无论如何...

* 副作用是指应用程序的任何隐藏效应,它可以改变状态/行为或在应用程序中引入错误,例如 IO。

**“纯”,它是撇号,因为它不是纯函数,并且代码可以收敛到纯版本,当没有控制台输出,只有数据操作时。

除此之外,为了纯粹,您需要使用处理副作用的 monad,它们容易出错,并将该错误与应用程序分开处理。


P
PranavKAndro

今天我遇到了多种解决方案。在 forEach 循环中运行异步等待函数。通过构建包装器,我们可以做到这一点。

More detailed explanation on how it works internally, for the native forEach and why it is not able to make a async function call and other details on the various methods are provided in link here

可以通过多种方式完成,如下所示,

方法1:使用包装器。

await (()=>{
     return new Promise((resolve,reject)=>{
       items.forEach(async (item,index)=>{
           try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
           count++;
           if(index === items.length-1){
             resolve('Done')
           }
         });
     });
    })();

方法二:和Array.prototype的泛型函数一样使用

Array.prototype.forEachAsync.js

if(!Array.prototype.forEachAsync) {
    Array.prototype.forEachAsync = function (fn){
      return new Promise((resolve,reject)=>{
        this.forEach(async(item,index,array)=>{
            await fn(item,index,array);
            if(index === array.length-1){
                resolve('done');
            }
        })
      });
    };
  }

用法 :

require('./Array.prototype.forEachAsync');

let count = 0;

let hello = async (items) => {

// Method 1 - Using the Array.prototype.forEach 

    await items.forEachAsync(async () => {
         try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
        count++;
    });

    console.log("count = " + count);
}

someAPICall = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("done") // or reject('error')
        }, 100);
    })
}

hello(['', '', '', '']); // hello([]) empty array is also be handled by default

方法3:

使用 Promise.all

  await Promise.all(items.map(async (item) => {
        await someAPICall();
        count++;
    }));

    console.log("count = " + count);

方法 4:传统的 for 循环或现代的 for 循环

// Method 4 - using for loop directly

// 1. Using the modern for(.. in..) loop
   for(item in items){

        await someAPICall();
        count++;
    }

//2. Using the traditional for loop 

    for(let i=0;i<items.length;i++){

        await someAPICall();
        count++;
    }


    console.log("count = " + count);

您的方法 1 和 2 只是应该使用 Promise.all 的错误实现 - 它们没有考虑到许多边缘情况中的任何一个。
@Bergi:感谢您的有效评论,请您解释一下为什么方法1和2不正确。它也起到了作用。这很好用。这就是说所有这些方法都是可能的,可以根据情况决定选择一种。我有相同的运行示例。
它在空数组上失败,它没有任何错误处理,可能还有更多问题。不要重新发明轮子。只需使用 Promise.all
在某些不可能的情况下,它会有所帮助。默认情况下,错误处理由 forEach api 完成,因此没有问题。它的照顾!
不,不存在 Promise.all 不可能但 async/await 是不可能的条件。不,forEach 绝对不会处理任何承诺错误。
r
richytong

您可以使用 Array.prototype.forEach,但 async/await 不是那么兼容。这是因为从异步回调返回的承诺预计会得到解决,但 Array.prototype.forEach 不会通过执行其回调来解决任何承诺。因此,您可以使用 forEach,但您必须自己处理承诺解决方案。

这是一种使用 Array.prototype.forEach 依次读取和打印每个文件的方法

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

  let promiseChain = Promise.resolve()
  files.forEach((file) => {
    promiseChain = promiseChain.then(() => {
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    })
  })
  await promiseChain
}

这是一种并行打印文件内容的方法(仍然使用 Array.prototype.forEach

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

  const promises = []
  files.forEach((file) => {
    promises.push(
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    )
  })
  await Promise.all(promises)
}

第一个 senario 非常适合需要在 serie 中运行并且您不能使用 for of 的循环
m
mikemaccana

目前 Array.forEach 原型属性不支持异步操作,但我们可以创建自己的 poly-fill 来满足我们的需求。

// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function 
async function asyncForEach(iteratorFunction){
  let indexer = 0
  for(let data of this){
    await iteratorFunction(data, indexer)
    indexer++
  }
}
// Append it as an Array prototype property
Array.prototype.asyncForEach = asyncForEach
module.exports = {Array}

就是这样!您现在可以在这些操作之后定义的任何数组上使用 async forEach 方法。

让我们测试一下...

// Nodejs style
// file: someOtherFile.js

const readline = require('readline')
Array = require('./asyncForEach').Array
const log = console.log

// Create a stream interface
function createReader(options={prompt: '>'}){
  return readline.createInterface({
    input: process.stdin
    ,output: process.stdout
    ,prompt: options.prompt !== undefined ? options.prompt : '>'
  })
}
// Create a cli stream reader
async function getUserIn(question, options={prompt:'>'}){
  log(question)
  let reader = createReader(options)
  return new Promise((res)=>{
    reader.on('line', (answer)=>{
      process.stdout.cursorTo(0, 0)
      process.stdout.clearScreenDown()
      reader.close()
      res(answer)
    })
  })
}

let questions = [
  `What's your name`
  ,`What's your favorite programming language`
  ,`What's your favorite async function`
]
let responses = {}

async function getResponses(){
// Notice we have to prepend await before calling the async Array function
// in order for it to function as expected
  await questions.asyncForEach(async function(question, index){
    let answer = await getUserIn(question)
    responses[question] = answer
  })
}

async function main(){
  await getResponses()
  log(responses)
}
main()
// Should prompt user for an answer to each question and then 
// log each question and answer as an object to the terminal

我们可以对其他一些数组函数做同样的事情,比如 map...

async function asyncMap(iteratorFunction){
  let newMap = []
  let indexer = 0
  for(let data of this){
    newMap[indexer] = await iteratorFunction(data, indexer, this)
    indexer++
  }
  return newMap
}

Array.prototype.asyncMap = asyncMap

... 等等 :)

需要注意的一些事项:

您的 iteratorFunction 必须是异步函数或承诺

在 Array.prototype. = 之前创建的任何数组都不会提供此功能


j
jgmjgm

要查看如何出错,请在方法末尾打印 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 中的一个示例,它将保持顺序,防止函数过早返回并在理论上保持最佳性能。

这将:

启动所有文件读取以并行发生。

通过使用 map 将文件名映射到等待的承诺来保留顺序。

按照数组定义的顺序等待每个承诺。

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

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

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

对于一次读取文件的版本,然后将在失败时停止,而不会浪费时间尝试读取更多文件。即使有一个精心设计的取消系统,也很难避免它在第一个文件上失败,但也已经读取了大多数其他文件。

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

该示例中也没有错误处理。如果某些事情要求它们要么全部成功显示,要么根本不显示,它不会那样做。

建议在每个阶段使用 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}`);
})();


B
Babakness

使用 Task、futurize 和一个可遍历的 List,你可以简单地做

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 operator

import { curry, flip } from 'ramda'

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

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

PS - 我没有在控制台上尝试这个代码,可能有一些错别字......“直自由泳,从圆顶顶部!”正如 90 后的孩子所说。 :-p


C
ChenZeTong

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

C
Craig Hicks

OP的原始问题

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

在@Bergi 的selected answer 中进行了一定程度的介绍,它展示了如何串行和并行处理。然而,并行性还有其他问题 -

订单——@chharvey 指出——

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

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

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

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

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

最简单的改进是像 @Bergi's answer 中那样执行完全并行,但要进行一些小的更改,以便在保持顺序的同时尽快打印每个文件

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 个。
就像一家商店一次只允许这么多客户(至少在 COVID 期间)。

首先介绍一个辅助函数——

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 返回几个属性

Promise 类型的 Promise

启动类型函数 ()=>void

promise 人生有两个阶段

承诺开始一项任务 承诺完成一项它已经开始的任务。

调用 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:用于打印

现在的区别是允许同时运行的承诺不超过 concurLimit 个。

重要的变量是

boots:要调用的函数数组以强制其相应的转换承诺。它仅用于分支 1。

set:随机访问容器中有承诺,因此一旦履行,它们就可以很容易地删除。此 contianer 仅在分支 1 中使用。

bootableProms:这些是最初在集合中的 smae 承诺,但它是一个数组而不是一个集合,并且该数组永远不会改变。它仅用于分支 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 playground sandbox 中作为可执行文件提供


A
Adam Zerner

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

我认为重要的是要解决为什么不会发生这种情况。

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

forEachfor 循环中,我们调用 cb(arr[i]),它最终是 logFile(file)logFile 函数内部有一个 await,所以 for 循环可能会在继续执行 i++ 之前等待这个 await

不,不会的。令人困惑的是,这不是 await 的工作方式。从 the docs

等待拆分执行流程,允许异步函数的调用者恢复执行。在 await 推迟了 async 函数的继续执行之后,随后的语句就会执行。如果此 await 是其函数执行的最后一个表达式,则继续通过向函数的调用者返回待处理的 Promise 以完成 await 的函数并恢复该调用者的执行。

因此,如果您有以下情况,则不会在 "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();

回到 forEachforEach 类似于 mainlogFile 类似于 logNumbersmain 不会因为 logNumbers 做了一些 await 而停止,forEach 也不会因为 logFile 做了一些 await 而停止。


S
Scott Rudiger

与 Antonio Val 的 p-iteration 类似,另一个 npm 模块是 async-af

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  // since AsyncAF accepts promises or non-promises, there's no need to await here
  const files = getFilePaths();

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

printFiles();

或者,async-af 有一个记录承诺结果的静态方法 (log/logAF):

const AsyncAF = require('async-af');
const fs = require('fs-promise');

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

  AsyncAF(files).forEach(file => {
    AsyncAF.log(fs.readFile(file, 'utf8'));
  });
}

printFiles();

但是,该库的主要优点是您可以链接异步方法来执行以下操作:

const aaf = require('async-af');
const fs = require('fs-promise');

const printFiles = () => aaf(getFilePaths())
  .map(file => fs.readFile(file, 'utf8'))
  .forEach(file => aaf.log(file));

printFiles();

async-af


H
Heretic Monkey

在 2022 年,我仍然建议使用外部库来处理所有这些异步流程。我已经为类似的事情创建了模块 alot🔗

你的例子是:

import fs from 'fs-promise'
import alot from 'alot'

async function printFiles () {
    const files = await getFilePaths() // Assume this works fine

    await alot(files)
        .forEachAsync(async file => {
            let content = await fs.readFile(file, 'utf8');
            console.log(content);
        })
        .toArrayAsync({ threads: 4 });
    }
}
printFiles()

对于简单的示例,异步 for..of 肯定会完成这项工作,但是一旦任务变得更加复杂,您就必须为此使用一些实用程序。

Alot 有许多其他方法可以链接,例如 mapAsyncfilterAsyncgroupAsync 等。

举个例子:

加载带有产品元数据的 JSON 文件

提取产品ID

从服务器加载产品

过滤价格 > 100 美元的商品

按价格升序排列

进入前 50 名

import fs from 'fs-promise'
import alot from 'alot'
import axios from 'axios'
import { File } from 'atma-io'

let paths = await getFilePaths();
let products = await alot(paths)
    .mapAsync(async path => await File.readAsync<IProductMeta>(path))
    .mapAsync(async meta => await axios.get(`${server}/api/product/${meta.productId}`))
    .mapAsync(resp => resp.data)
    .filterAsync(product => product.price > 100)
    .sortBy(product => product.price, 'asc')
    .takeAsync(50)
    .toArrayAsync({ threads: 5, errors: 'include' });

threads: 4 是什么? JS没有线程
@Bergi但是底层有。所有这些 async\await 故事都意味着事件循环一直等到它返回结果。通过定义 threads,我们设置了启动并行任务的数量,其他任务将等待至少一个任务(fs、网络、工作人员等)准备好。
W
Wojciech Maj

如果您想同时迭代所有元素:

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

如果您想非并发地迭代所有元素(例如,当您的映射函数有副作用或一次在所有数组元素上运行映射器时资源成本太高):

选项 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);
  }
}

您的选项 a 涉及 Promise constructor antipattern
M
Matt Janssen

如果你不能使用 async/await(IE11、旧打包器等)那么你可以试试这个递归函数。我使用 fetch 作为异步调用,但您可以使用任何返回承诺的函数。

var urlsToGet = ['https://google.com', 'https://yahoo.com'];

fetchOneAtATime(urlsToGet);

function fetchOneAtATime(urls) {
    if (urls.length === 0) {
        return;
    }
    fetch(urls[0]).finally(() => fetchOneAtATime(urls.slice(1)));
}

最好在第一次调用 .shift() 之前检查 urls.length,最好使用 urls[0]urls.slice(1) 而不是清空传递给函数的数组。
为什么使用 finally 而不是 then?与 async/await 不同,这将忽略错误
这将是如果您想进行每次提取,而不管前面的调用是否成功。空检查和不改变数组的好主意! ✔