Javascript 在 forEach 循环中使用 async/await

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/37576685/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 20:28:44  来源:igfitidea点击:

Using async/await with a forEach loop

javascriptnode.jspromiseasync-awaitecmascript-2017

提问by saadq

Are there any issues with using async/awaitin a forEachloop? I'm trying to loop through an array of files and awaiton the contents of each file.

在循环中使用async/有什么问题吗?我正在尝试遍历文件数组和每个文件的内容。awaitforEachawait

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

This code does work, but could something go wrong with this? I had someone tell me that you're not supposed to use async/awaitin a higher order function like this, so I just wanted to ask if there was any issue with this.

这段代码确实有效,但会不会有什么问题?有人告诉我你不应该在这样的高阶函数中使用async/ await,所以我只是想问问这是否有任何问题。

回答by Bergi

Sure the code does work, but I'm pretty sure it doesn't do what you expect it to do. It just fires off multiple asynchronous calls, but the printFilesfunction does immediately return after that.

当然代码确实有效,但我很确定它不会做你期望它做的事情。它只是触发多个异步调用,但printFiles此后该函数会立即返回。

Reading in sequence

按顺序阅读

If you want to read the files in sequence, you cannot use forEachindeed. Just use a modern for … ofloop instead, in which awaitwill work as expected:

如果要按顺序读取文件,确实无法使用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);
  }
}

Reading in parallel

并行阅读

If you want to read the files in parallel, you cannot use forEachindeed. Each of the asynccallback function calls does return a promise, but you're throwing them away instead of awaiting them. Just use mapinstead, and you can await the array of promises that you'll get with Promise.all:

如果要并行读取文件,forEach确实无法使用。每个async回调函数调用都会返回一个承诺,但您将它们扔掉而不是等待它们。只需使用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)
  }));
}

回答by Francisco Mateo

With ES2018, you are able to greatly simplify all of the above answers to:

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

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

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

See spec: proposal-async-iteration

参见规范:proposal-async-iteration



2018-09-10: This answer has been getting a lot attention recently, please see Axel Rauschmayer's blog post for further information about asynchronous iteration: ES2018: asynchronous iteration

2018-09-10:这个答案最近引起了很多关注,有关异步迭代的更多信息,请参阅Axel Rauschmayer的博客文章:ES2018:异步迭代

回答by Timothy Zorn

Instead of Promise.allin conjunction with Array.prototype.map(which does not guarantee the order in which the Promises are resolved), I use Array.prototype.reduce, starting with a resolved Promise:

而不是Promise.all结合 with Array.prototype.map(这不保证Promises 的解析顺序),我使用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());
}

回答by Antonio Val

The p-iterationmodule on npm implements the Array iteration methods so they can be used in a very straightforward way with async/await.

npm 上的p-iteration模块实现了 Array 迭代方法,因此它们可以通过 async/await 以非常简单的方式使用。

An example with your case:

你的情况的一个例子:

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

回答by Matt

Here are some forEachAsyncprototypes. Note you'll need to awaitthem:

这里有一些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));
}

Notewhile you may include this in your own code, you should not include this in libraries you distribute to others (to avoid polluting their globals).

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

回答by chharvey

In addition to @Bergi's answer, I'd like to offer a third alternative. It's very similar to @Bergi's 2nd example, but instead of awaiting each readFileindividually, you create an array of promises, each which you await at the end.

除了@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);
}

Note that the function passed to .map()does not need to be async, since fs.readFilereturns a Promise object anyway. Therefore promisesis an array of Promise objects, which can be sent to Promise.all().

请注意,传递给的函数.map()不需要是async,因为fs.readFile无论如何都会返回一个 Promise 对象。因此promises是一个 Promise 对象数组,可以发送到Promise.all().

In @Bergi's answer, the console may log file contents in the order they're read. For example if a really small file finishes reading before a really large file, it will be logged first, even if the small file comes afterthe large file in the filesarray. However, in my method above, you are guaranteed the console will log the files in the same order as the provided array.

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

回答by myDoggyWritesCode

Bergi's solutionworks nicely when fsis promise based. You can use bluebird, fs-extraor fs-promisefor this.

fs基于承诺时,Bergi 的解决方案工作得很好。您可以使用bluebird,fs-extrafs-promise为此。

However, solution fornode's native fslibary is as follows:

但是,node原生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;
    }));

Note:require('fs')compulsorily takes function as 3rd arguments, otherwise throws error:

注意:require('fs')强制将函数作为第三个参数,否则抛出错误:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

回答by Hooman Askari

Both the solutions above work, however, Antonio's does the job with less code, here is how it helped me resolve data from my database, from several different child refs and then pushing them all into an array and resolving it in a promise after all is done:

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

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

回答by Jay Edwards

it's pretty painless to pop a couple methods in a file that will handle asynchronous data in a serialized order and give a more conventional flavour to your code. For example:

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

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

now, assuming that's saved at './myAsync.js' you can do something similar to the below in an adjacent file:

现在,假设它保存在 './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));
}

回答by lukaswilkeer

Like @Bergi's response, but with one difference.

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

Promise.allrejects all promises if one gets rejected.

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

So, use a recursion.

所以,使用递归。

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

PS

聚苯乙烯

readFilesQueueis outside of printFilescause the side effect* introduced by console.log, it's better to mock, test, and or spy so, it's not cool to have a function that returns the content(sidenote).

readFilesQueue不是printFiles由 引入的副作用*的原因之外,console.log最好是模拟、测试和/或监视,因此,拥有返回内容的函数并不酷(旁注)。

Therefore, the code can simply be designed by that: three separated functions that are "pure"** and introduce no side effects, process the entire list and can easily be modified to handle failed cases.

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

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)

Future edit/current state

未来编辑/当前状态

Node supports top-level await (this doesn't have a plugin yet, won't have and can be enabled via harmony flags), it's cool but doesn't solve one problem (strategically I work only on LTS versions). How to get the files?

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

Using composition. Given the code, causes to me a sensation that this is inside a module, so, should have a function to do it. If not, you should use an IIFE to wrap the role code into an async function creating simple module that's do all for you, or you can go with the right way, there is, composition.

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

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

Note that the name of variable changes due to semantics. You pass a functor (a function that can be invoked by another function) and recieves a pointer on memory that contains the initial block of logic of the application.

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

But, if's not a module and you need to export the logic?

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

Wrap the functions in a async function.

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

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

Or change the names of variables, whatever...

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



*by side effect menans any colacteral effect of application that can change the statate/behaviour or introuce bugs in the application, like IO.

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

**by "pure", it's in apostrophe since the functions it's not pure and the code can be converged to a pure version, when there's no console output, only data manipulations.

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

Aside this, to be pure, you'll need to work with monads that handles the side effect, that are error prone, and treats that error separately of the application.

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