使用async / await和forEach循环

问题描述 投票:696回答:14

async循环中使用await / forEach有什么问题吗?我正在尝试遍历每个文件的内容上的文件和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,所以我只是想问这个是否有任何问题。

javascript node.js promise async-await ecmascript-2017
14个回答
1432
投票

当然代码确实有效,但我很确定它没有按照你的预期去做。它只是触发多个异步调用,但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回调函数调用都会返回一个承诺,但是你把它们扔掉而不是等待它们。只需使用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)
  }));
}

1
投票

除了@Bergi’s answer,我还想提供第三种选择。它与@ 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()的函数不需要是async,因为fs.readFile无论如何都返回一个Promise对象。因此promises是一个Promise对象的数组,可以发送到Promise.all()

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


1
投票

目前,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必须是异步函数或promise
  • Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>之前创建的任何数组都不具备此功能

1
投票

Bergi's solution承诺时,fs运作良好。你可以使用bluebirdfs-extrafs-promise

但是,节点的本地qazxsw poi库的解决方案如下:

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

0
投票

与Antonio Val的TypeError [ERR_INVALID_CALLBACK]: Callback must be a function 类似,另一个npm模块是p-iteration

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(); 有一个静态方法(log / logAF),它记录了promises的结果:

async-af

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

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


-3
投票

我会使用经过充分测试的(每周数百万次下载)async-afpify模块。如果您不熟悉异步模块,我强烈建议您查看async。我已经看到多个开发人员浪费时间重新创建它的方法,或者更糟糕的是,当高阶异步方法简化代码时,制作难以维护的异步代码。


112
投票

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

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

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

见规格:https://github.com/tc39/proposal-async-iteration


2018-09-10:这个答案最近受到了很多关注,请参阅Axel Rauschmayer的博客文章,了解有关异步迭代的更多信息:http://2ality.com/2016/10/asynchronous-iteration.html


36
投票

而不是Promise.allArray.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());
}

26
投票

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

15
投票

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

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


2
投票

然而,上述两种解决方案的工作方式都很少,安东尼奥用较少的代码完成工作,以下是它如何帮助我从数据库中解析数据,从几个不同的子引用,然后将它们全部推入数组并在一个承诺中解决它毕竟是完成:

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

2
投票

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

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

1
投票

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

在真正的await循环中使用for将确保所有异步调用逐个执行。并且forEach + async方式将同时触发所有承诺,这更快但有时不堪重负(如果您进行一些数据库查询或访问某些具有数量限制的Web服务并且不希望一次触发100,000个呼叫)。

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

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

1
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.