it-swarm.com.de

Verwenden von async/await mit einer forEach-Schleife

Gibt es Probleme bei der Verwendung von async/await in einer forEach-Schleife? Ich versuche, ein Array von Dateien und await für den Inhalt jeder Datei durchzugehen. 

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

Dieser Code funktioniert, aber könnte damit etwas schief gehen? Ich hatte jemanden sagen lassen, dass Sie async/await nicht in einer höheren Reihenfolge verwenden sollten.

566
saadq

Sicher, der Code funktioniert, aber ich bin ziemlich sicher, dass er nicht das tut, was Sie erwarten. Es werden nur mehrere asynchrone Aufrufe ausgelöst, aber die Funktion printFiles kehrt sofort zurück.

Wenn Sie die Dateien nacheinander lesen möchten, können Sie nicht forEachverwenden. Verwenden Sie stattdessen eine moderne for … of-Schleife, in der await wie erwartet funktioniert:

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

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

Wenn Sie die Dateien parallel lesen möchten, können Sie nicht forEachverwenden. Jeder Aufruf der async Callback-Funktion gibt ein Versprechen zurück, aber Sie werfen sie weg, statt sie abzuwarten. Verwenden Sie stattdessen einfach map, und Sie können auf die Versprechen warten, die Sie mit Promise.all erhalten:

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

  await Promise.all(files.map(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  }));
}
1180
Bergi

Mit ES2018 können Sie alle oben genannten Antworten auf:

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

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

Siehe spec: https://github.com/tc39/proposal-async-iteration


2018-09-10: Diese Antwort hat in letzter Zeit viel Aufmerksamkeit erhalten. Weitere Informationen zur asynchronen Iteration finden Sie im Blog-Beitrag von Axel Rauschmayer: http://2ality.com/2016/10/asynchronous-iteration.html

83
Francisco Mateo

Für mich ist die Verwendung von Promise.all() mit map() etwas schwer zu verstehen und verbos, aber wenn Sie es in einfachem JS machen wollen, ist dies der beste Schlag, denke ich.

Wenn Sie nichts dagegen haben, ein Modul hinzuzufügen, habe ich die Array-Iterationsmethoden implementiert, sodass sie mit async/await auf sehr unkomplizierte Weise verwendet werden können.

Ein Beispiel für Ihren Fall:

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

printFiles()

p-Iteration

22
Antonio Val

Anstelle von Promise.all in Verbindung mit Array.prototype.map (was nicht die Reihenfolge garantiert, in der die Promises aufgelöst werden), verwende ich Array.prototype.reduce, beginnend mit einer aufgelösten 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());
}
20
Timothy Zorn

Hier sind einige forEach async-Prototypen:

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

Die beiden oben genannten Lösungen funktionieren, jedoch erledigt Antonio's die Aufgabe mit weniger Code. So hat es mir geholfen, Daten aus meiner Datenbank von mehreren verschiedenen untergeordneten Refs zu lösen und dann alle in ein Array zu verschieben und es in einem Versprechen aufzulösen erledigt:

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
Hooman Askari

es ist ziemlich schmerzlos, ein paar Methoden in einer Datei zu platzieren, die asynchrone Daten in einer serialisierten Reihenfolge verarbeiten und Ihrem Code eine konventionellere Note verleihen. Zum Beispiel:

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

vorausgesetzt, das ist bei './myAsync.js' gespeichert, können Sie etwas Ähnliches wie in einer benachbarten Datei tun:

...
/* 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));
}
2
Jay Edwards

Zusätzlich zu @ Bergis Antwort möchte ich eine dritte Alternative anbieten. Es ist dem zweiten Beispiel von @ Bergi sehr ähnlich, aber anstatt jedes readFile individuell abzuwarten, erstellen Sie eine Reihe von Versprechungen, auf die Sie am Ende warten.

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

Beachten Sie, dass die an .map() übergebene Funktion nicht async sein muss, da fs.readFile ohnehin ein Promise-Objekt zurückgibt. Daher ist promises ein Array von Promise-Objekten, die an Promise.all() gesendet werden können.

In der Antwort von @ Bergi protokolliert die Konsole möglicherweise den Inhalt der Datei außerhalb der Reihenfolge. Wenn zum Beispiel eine wirklich kleine Datei vor einer wirklich großen Datei gelesen wird, wird sie zuerst protokolliert, selbst wenn die kleine Datei after die große Datei im files-Array ist. Bei meiner oben beschriebenen Methode wird jedoch garantiert, dass die Konsole die Dateien in der gleichen Reihenfolge protokolliert, in der sie gelesen werden.

1
chharvey

Derzeit unterstützt die Array.forEach-Prototypeigenschaft keine asynchronen Vorgänge, wir können jedoch eine eigene Polyfüllung erstellen, um unseren Anforderungen gerecht zu werden.

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

Und das ist es! Sie haben jetzt eine asynchrone forEach-Methode für alle Arrays verfügbar, die danach für Operationen definiert werden.

Lass es uns testen ...

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

Wir könnten dasselbe für einige der anderen Array-Funktionen wie map tun ...

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

... und so weiter :)

Einige Dinge zu beachten:

  • Ihre iteratorFunction muss eine asynchrone Funktion oder ein Versprechen sein
  • Bei Arrays, die vor Array.prototype.<yourAsyncFunc> = <yourAsyncFunc> erstellt wurden, steht diese Funktion nicht zur Verfügung
1
Beau

Mit Task, Futurize und einer durchlaufbaren Liste können Sie dies einfach tun

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

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

So würden Sie das einrichten

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)

Eine andere Möglichkeit, den gewünschten Code zu strukturieren, wäre

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

Oder vielleicht noch funktionaler

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

Dann von der übergeordneten Funktion

async function main() {
  /* awesome code with side-effects before */
  printFiles( await getFiles() );
  /* awesome code with side-effects after */
}

Wenn Sie wirklich mehr Flexibilität beim Codieren wünschen, können Sie dies einfach tun (zum Spaß verwende ich den vorgeschlagenen Operator Pipe Forward )

import { curry, flip } from 'ramda'

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

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

PS - Ich habe diesen Code nicht auf der Konsole ausprobiert, vielleicht ein paar Tippfehler ... "Freestyle, ganz oben auf der Kuppel! wie die Kinder der 90er Jahre sagen würden. :-p

1
Babakness

Bergis Lösung funktioniert gut, wenn fs Versprechen basiert. Sie können hierfür bluebird, fs-extra oder fs-promise verwenden.

Die Lösung für die native fs-Bibliothek von node lautet jedoch wie folgt:

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

Hinweis: require('fs') übernimmt zwangsweise die Funktion als 3. Argument, ansonsten wird ein Fehler ausgegeben:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
0

Ähnlich wie p-iteration von Antonio Val ist ein alternatives npm-Modul 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();

Alternativ dazu verfügt async-af über eine statische Methode (log/logAF), die die Ergebnisse von Versprechen protokolliert:

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

Der Hauptvorteil der Bibliothek besteht jedoch darin, dass Sie asynchrone Methoden so verketten können, dass sie wie folgt vorgehen:

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

0
Scott Rudiger

Ein wichtiges caveat ist: Die await + for .. of-Methode und die forEach + async-Methode haben unterschiedliche Auswirkungen. 

Wenn Sie await in einer echten for-Schleife haben, wird sichergestellt, dass alle asynchronen Aufrufe nacheinander ausgeführt werden. Die forEach + async-Methode löst gleichzeitig alle Versprechungen aus. Dies ist zwar schneller, aber manchmal überfordert (, wenn Sie eine DB-Abfrage durchführen oder einige Webservices mit Volumeneinschränkungen besuchen und nicht 100.000 Anrufe gleichzeitig auslösen möchten ). 

Sie können auch reduce + promise (weniger elegant) verwenden, wenn Sie async/await nicht verwenden und sicherstellen möchten, dass die Dateien nacheinander gelesen werden

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

Sie können auch forEachAsync erstellen, um zu helfen, im Grunde jedoch dasselbe für die zugrundeliegende Schleife zu verwenden.

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