Verwenden von async / await mit einer forEach-Schleife

1129

Gibt es Probleme bei der Verwendung von async/ awaitin einer forEachSchleife? Ich versuche, ein Array von Dateien und awaitden Inhalt jeder Datei zu durchlaufen .

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, der mir sagte, dass Sie async/ awaitin einer Funktion höherer Ordnung wie dieser nicht verwenden sollen , also wollte ich nur fragen, ob es ein Problem damit gibt.

saadq
quelle

Antworten:

2148

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

Nacheinander lesen

Wenn Sie die Dateien nacheinander lesen möchten, können Sie sie in derforEach Tat nicht verwenden . Verwenden Sie for … ofstattdessen einfach eine moderne Schleife, in der awaitwie erwartet funktioniert:

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

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

Parallel lesen

Wenn Sie die Dateien parallel lesen möchten, können Sie sie in derforEach Tat nicht verwenden . Jeder asyncAufruf der Rückruffunktion gibt ein Versprechen zurück, aber Sie werfen sie weg, anstatt auf sie zu warten. Verwenden Sie mapstattdessen einfach , und Sie können auf die Reihe von Versprechungen warten, die Sie erhalten 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)
  }));
}
Bergi
quelle
33
Könnten Sie bitte erklären, warum funktioniert for ... of ...?
Demonbane
84
ok ich weiß warum ... Die Verwendung von Babel transformiert async/ awaitin die Generatorfunktion und die Verwendung forEachbedeutet, dass jede Iteration eine individuelle Generatorfunktion hat, die nichts mit den anderen zu tun hat. Sie werden also unabhängig ausgeführt und haben keinen Zusammenhang next()mit anderen. Tatsächlich for()funktioniert eine einfache Schleife auch, weil sich die Iterationen auch in einer einzigen Generatorfunktion befinden.
Demonbane
21
@Demonbane: Kurz gesagt, weil es so konzipiert wurde, dass es funktioniert :-) awaitsetzt die aktuelle Funktionsbewertung einschließlich aller Kontrollstrukturen aus. Ja, diesbezüglich ist es den Generatoren ziemlich ähnlich (weshalb sie zum Polyfill-Async / Warten verwendet werden).
Bergi
3
@ arve0 Nicht wirklich, eine asyncFunktion unterscheidet sich stark von einem PromiseExecutor-Rückruf, aber ja, der mapRückruf gibt in beiden Fällen ein Versprechen zurück.
Bergi
5
Wenn Sie etwas über JS-Versprechen erfahren möchten, verwenden Sie stattdessen eine halbe Stunde, um Latein zu übersetzen. Hoffe du bist stolz @Bergi;)
Félix Gagnon-Grenier
189

Mit ES2018 können Sie alle oben genannten Antworten erheblich vereinfachen, um:

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

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

Siehe Spezifikation: Vorschlag-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: ES2018: asynchrone Iteration

Francisco Mateo
quelle
4
Upvoted, wäre großartig, wenn Sie in Ihrer Antwort einen Link zu der Spezifikation für jeden einfügen könnten , der mehr über asynchrone Iteration erfahren möchte.
Saadq
8
Sollte es nicht Inhalt statt Datei im Iterator sein
FluffyBeing
10
Warum stimmen die Leute dieser Antwort zu? Schauen Sie sich die Antwort, die Frage und den Vorschlag genauer an. Nach ofsollte die asynchrone Funktion sein, die ein Array zurückgibt. Es funktioniert nicht und Francisco sagte;
Yevhenii Herasymchuk
3
Stimme @AntonioVal voll und ganz zu. Es ist keine Antwort.
Yevhenii Herasymchuk
2
Obwohl ich der Meinung bin, dass dies keine Antwort ist, ist die Abstimmung eines Vorschlags eine Möglichkeit, seine Popularität zu steigern und ihn möglicherweise früher verfügbar zu machen, um ihn später zu verwenden.
Robert Molina
61

Anstelle von Promise.allin Verbindung mit Array.prototype.map(was nicht die Reihenfolge garantiert, in der die Promises aufgelöst werden) verwende ich Array.prototype.reduce, beginnend mit einem 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());
}
Timothy Zorn
quelle
1
Das funktioniert perfekt, vielen Dank. Können Sie erklären, was hier mit Promise.resolve()und passiert await promise;?
parrker9
1
Das ist ziemlich cool. Habe ich Recht, wenn ich denke, dass die Dateien in der richtigen Reihenfolge und nicht auf einmal gelesen werden?
GollyJer
1
@ parrker9 Promise.resolve()gibt ein bereits aufgelöstes PromiseObjekt zurück, sodass zunächst ein Objekt erstellt werden reducemuss Promise. await promise;wartet, bis der letzte Promisein der Kette aufgelöst ist. @GollyJer Die Dateien werden nacheinander nacheinander verarbeitet.
Timothy Zorn
Sehr coole Verwendung von Reduzieren, danke für den Kommentar! Ich möchte nur darauf hinweisen, dass diese im Gegensatz zu einigen anderen in den Kommentaren erwähnten Methoden synchron ist, was bedeutet, dass die Dateien nacheinander und nicht parallel gelesen werden (da die nächste Iteration der Reduktionsfunktion von der vorherigen abhängt Iteration muss synchron sein).
Shay Yzhakov
1
@Shay, du meinst sequentiell, nicht synchron. Dies ist immer noch asynchron - wenn andere Dinge geplant sind, werden sie zwischen den Iterationen hier ausgeführt.
Timothy Zorn
32

Das p-Iterationsmodul auf npm implementiert die Array-Iterationsmethoden, sodass sie mit async / await auf sehr einfache 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);
  });
})();
Antonio Val
quelle
1
Ich mag das, da es die gleichen Funktionen / Methoden wie JS selbst hat - in meinem Fall brauchte ich someeher als forEach. Vielen Dank!
Mikemaccana
25

Hier sind einige forEachAsyncPrototypen. Beachten Sie, dass Sie sie benötigen 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));
}

Beachten Sie, dass Sie dies möglicherweise in Ihren eigenen Code aufnehmen, dies jedoch nicht in Bibliotheken aufnehmen sollten, die Sie an andere verteilen (um eine Verschmutzung ihrer globalen Daten zu vermeiden).

Matt
quelle
1
Obwohl ich zögern würde, Dinge direkt zum Prototyp hinzuzufügen, ist dies eine nette asynchrone Implementierung für
jede
2
Solange der Name in Zukunft eindeutig ist (wie ich ihn verwenden würde _forEachAsync), ist dies vernünftig. Ich denke auch, dass es die schönste Antwort ist, da es viel Boilerplate-Code spart.
Mikemaccana
1
@estus Damit soll vermieden werden, dass der Code anderer Personen verschmutzt wird. Wenn der Code zu unserer persönlichen Organisation gehört und sich die Globals in einer gut identifizierten Datei befinden ( globals.jswäre gut), können wir Globals nach Belieben hinzufügen.
Mikemaccana
1
@mikemaccana Das soll allgemein akzeptierte schlechte Praktiken vermeiden. Dies ist zwar möglich, solange Sie nur Erstanbieter-Code verwenden, was selten vorkommt. Das Problem ist, dass es bei der Verwendung von Bibliotheken von Drittanbietern einen anderen Typen geben kann, der sich genauso fühlt und dieselben globalen Elemente ändert, nur weil dies zu der Zeit, als eine Bibliothek geschrieben wurde, eine gute Idee zu sein schien.
Estus Flask
1
@estus Sicher. Ich habe der Frage eine Warnung hinzugefügt, um die (nicht besonders produktive) Diskussion hier zu speichern.
Mikemaccana
6

Zusätzlich zu @ Bergis Antwort möchte ich eine dritte Alternative anbieten. Es ist dem zweiten Beispiel von @ Bergi sehr ähnlich, aber anstatt jedes readFileeinzeln zu erwarten , 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 übergebene Funktion .map()nicht sein muss async, da fs.readFileohnehin ein Promise-Objekt zurückgegeben wird. Daher promisesgibt es ein Array von Promise-Objekten, an die gesendet werden kann Promise.all().

In der Antwort von @ Bergi protokolliert die Konsole möglicherweise den Dateiinhalt in der Reihenfolge, in der er gelesen wurde. Wenn beispielsweise eine wirklich kleine Datei vor einer wirklich großen Datei vollständig gelesen wird, wird sie zuerst protokolliert, auch wenn die kleine Datei nach der großen Datei im filesArray kommt. Bei meiner obigen Methode ist jedoch garantiert, dass die Konsole die Dateien in derselben Reihenfolge wie das bereitgestellte Array protokolliert.

chharvey
quelle
1
Ich bin mir ziemlich sicher, dass Sie falsch liegen: Ich bin mir ziemlich sicher, dass Ihre Methode die Dateien auch in unregelmäßiger Reihenfolge lesen kann. Ja, die Ausgabe wird in der richtigen Reihenfolge protokolliert (aufgrund der await Promise.all), aber die Dateien wurden möglicherweise in einer anderen Reihenfolge gelesen, was Ihrer Aussage widerspricht: "Sie werden garantiert, dass die Konsole die Dateien in derselben Reihenfolge protokolliert, in der sie sind." lesen".
Venryx
1
@Venryx Du hast recht, danke für die Korrektur. Ich habe meine Antwort aktualisiert.
Chharvey
5

Bergis Lösung funktioniert gut, wenn sie auf fsVersprechen basiert. Sie können verwendet werden bluebird, fs-extraoder fs-promisefür diese.

Die Lösung für die native fsBibliothek des Knotens 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: Die require('fs') Funktion wird zwangsweise als drittes Argument verwendet, andernfalls wird ein Fehler ausgegeben:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
myDoggyWritesCode
quelle
4

Beide oben genannten Lösungen funktionieren jedoch, Antonio's erledigt die Arbeit mit weniger Code. Hier ist, wie es mir geholfen hat, Daten aus meiner Datenbank, aus mehreren verschiedenen untergeordneten Refs aufzulösen und sie dann alle in ein Array zu verschieben und sie schließlich 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)))
Hooman Askari
quelle
3

Es ist ziemlich schmerzlos, ein paar Methoden in eine Datei einzufügen, 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;
  };
};

Angenommen, dies ist unter './myAsync.js' gespeichert, können Sie in einer benachbarten Datei etwas Ähnliches wie das Folgende 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));
}
Jay Edwards
quelle
2
Kleiner Nachtrag, vergessen Sie nicht, Ihre Wartezeiten / Asyncs in Try / Catch-Blöcke zu packen !!
Jay Edwards
3

Wie @ Bergis Antwort, aber mit einem Unterschied.

Promise.all lehnt alle Versprechen ab, wenn man abgelehnt wird.

Verwenden Sie also eine Rekursion.

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

readFilesQueueliegt außerhalb der printFilesUrsache der durch * eingeführten Nebenwirkungconsole.log . Es ist besser, sich zu verspotten, zu testen und / oder auszuspionieren. Es ist also nicht cool, eine Funktion zu haben, die den Inhalt zurückgibt (Nebenbemerkung).

Daher kann der Code einfach so gestaltet werden: Drei getrennte Funktionen, die "rein" ** sind und keine Nebenwirkungen verursachen, die gesamte Liste verarbeiten und leicht geändert werden können, um fehlgeschlagene Fälle zu behandeln.

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)

Zukünftige Bearbeitung / aktueller Status

Node unterstützt das Warten auf oberster Ebene (dies hat noch kein Plugin, wird es nicht haben und kann über Harmony-Flags aktiviert werden), es ist cool, löst aber kein Problem (strategisch arbeite ich nur an LTS-Versionen). Wie bekomme ich die Dateien?

Komposition verwenden. Angesichts des Codes entsteht für mich das Gefühl, dass sich dieses in einem Modul befindet, also sollte es eine Funktion haben, dies zu tun. Wenn nicht, sollten Sie ein IIFE verwenden, um den Rollencode in eine asynchrone Funktion zu verpacken und ein einfaches Modul zu erstellen, das alles für Sie erledigt, oder Sie können den richtigen Weg einschlagen, es gibt Komposition.

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

Beachten Sie, dass sich der Name der Variablen aufgrund der Semantik ändert. Sie übergeben einen Funktor (eine Funktion, die von einer anderen Funktion aufgerufen werden kann) und erhalten einen Zeiger auf den Speicher, der den anfänglichen Logikblock der Anwendung enthält.

Aber wenn es kein Modul ist und Sie die Logik exportieren müssen?

Wickeln Sie die Funktionen in eine asynchrone Funktion ein.

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

Oder ändern Sie die Namen von Variablen, was auch immer ...


* Nebeneffekt Menans jede kolakterale Wirkung der Anwendung, die den Status / das Verhalten ändern oder Fehler in der Anwendung hervorrufen kann, wie z. B. IO.

** von "pure" ist es in Apostroph, da die Funktionen nicht rein sind und der Code zu einer reinen Version konvergiert werden kann, wenn keine Konsolenausgabe erfolgt, sondern nur Datenmanipulationen.

Abgesehen davon müssen Sie, um rein zu sein, mit Monaden arbeiten, die den Nebeneffekt behandeln, fehleranfällig sind und diesen Fehler separat von der Anwendung behandeln.

lukaswilkeer
quelle
2

Eine wichtige Einschränkung ist: Die await + for .. ofMethode und der forEach + asyncWeg haben tatsächlich unterschiedliche Auswirkungen.

Wenn Sie sich awaitin einer echten forSchleife befinden, wird sichergestellt, dass alle asynchronen Aufrufe einzeln ausgeführt werden. Und der forEach + asyncWeg löst alle Versprechen gleichzeitig aus, was schneller, aber manchmal überfordert ist ( wenn Sie eine DB-Abfrage durchführen oder einige Webdienste mit Volumenbeschränkungen besuchen und nicht 100.000 Anrufe gleichzeitig auslösen möchten).

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

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

Oder Sie können ein forEachAsync erstellen, um zu helfen, aber im Grunde dasselbe für die zugrunde liegende Schleife verwenden.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
LeOn - Han Li
quelle
Schauen Sie sich an, wie Sie Methoden in Javascript für Array.prototype und Object.prototype definieren, damit sie nicht in for in loop angezeigt werden . Außerdem sollten Sie wahrscheinlich dieselbe Iteration wie native verwenden forEach- auf Indizes zugreifen, anstatt sich auf die Iterierbarkeit zu verlassen - und den Index an den Rückruf übergeben.
Bergi
Sie können Array.prototype.reduceeine asynchrone Funktion verwenden. Ich habe ein Beispiel in meiner Antwort gezeigt: stackoverflow.com/a/49499491/2537258
Timothy Zorn
2

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

Hier ist, wie Sie dies einrichten würden

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 ausgerichtet

// 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 bei der Codierung wünschen, können Sie dies einfach tun (zum Spaß verwende ich den vorgeschlagenen Pipe Forward-Operator ).

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, habe möglicherweise Tippfehler ... "Straight Freestyle, ganz oben auf der Kuppel!" wie die 90er Kinder sagen würden. :-p

Babakness
quelle
2

Derzeit unterstützt die Prototyp-Eigenschaft Array.forEach keine asynchronen Vorgänge, aber wir können unsere eigene Polyfüllung erstellen, um unsere Anforderungen zu erfüllen.

// 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 das gleiche 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 zuvor erstellten Arrays ist Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>diese Funktion nicht verfügbar
Beau
quelle
2

Einfach zur ursprünglichen Antwort hinzufügen

  • Die Syntax des parallelen Lesens in der ursprünglichen Antwort ist manchmal verwirrend und schwer zu lesen. Vielleicht können wir sie in einem anderen Ansatz schreiben
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);
}
  • Für den sequentiellen Betrieb, nicht nur für ... von , funktioniert auch die normale for-Schleife
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);
  }
}
gsaandy
quelle
1

Heute bin ich auf mehrere Lösungen dafür gestoßen. Ausführen der asynchronen Wartefunktionen in der forEach-Schleife. Indem wir den Wrapper umbauen, können wir dies erreichen.

Eine ausführlichere Erklärung, wie es intern funktioniert, für das native forEach und warum es keinen asynchronen Funktionsaufruf ausführen kann, und weitere Details zu den verschiedenen Methoden finden Sie unter dem Link hier

Die verschiedenen Möglichkeiten, wie dies getan werden kann, sind wie folgt:

Methode 1: Verwenden des Wrappers.

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

Methode 2: Verwenden derselben Funktion als generische Funktion von 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');
            }
        })
      });
    };
  }

Verwendungszweck :

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

Methode 3:

Verwenden von Promise.all

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

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

Methode 4: Traditionell für Schleife oder modern für Schleife

// 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);
PranavKAndro
quelle
Ihre Methoden 1 und 2 sind einfach falsche Implementierungen, bei denen Promise.allsie hätten verwendet werden sollen - sie berücksichtigen keinen der vielen Randfälle.
Bergi
@Bergi: Danke für die gültigen Kommentare. Würden Sie mir bitte erklären, warum Methode 1 und 2 falsch sind. Es dient auch dem Zweck. Das funktioniert sehr gut. Dies bedeutet, dass alle diese Methoden möglich sind, basierend auf der Situation, in der man sich für eine entscheiden kann. Ich habe das laufende Beispiel dafür.
PranavKAndro
Es schlägt auf leeren Arrays fehl, es hat keine Fehlerbehandlung und wahrscheinlich mehr Probleme. Das Rad nicht neu erfinden. Einfach benutzen Promise.all.
Bergi
Unter bestimmten Bedingungen, unter denen dies nicht möglich ist, ist dies hilfreich. Auch die Fehlerbehandlung wird standardmäßig von forEach API durchgeführt, sodass keine Probleme auftreten. Es ist erledigt!
PranavKAndro
Nein, es gibt keine Bedingungen, unter denen dies Promise.allnicht möglich ist, aber async/ awaitist. Und nein, behandelt forEachabsolut keine Versprechensfehler.
Bergi
1

Diese Lösung ist auch speicheroptimiert, sodass Sie sie auf 10.000 Datenelementen und Anforderungen ausführen können. Einige der anderen Lösungen hier stürzen den Server bei großen Datenmengen ab.

In TypeScript:

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

Wie benutzt man?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
Oliver Dixon
quelle
1

Sie können verwenden Array.prototype.forEach, aber async / await ist nicht so kompatibel. Dies liegt daran, dass das von einem asynchronen Rückruf zurückgegebene Versprechen voraussichtlich aufgelöst wird, jedoch Array.prototype.forEachkeine Versprechen aus der Ausführung seines Rückrufs auflöst. Dann können Sie forEach verwenden, aber Sie müssen die Versprechen-Lösung selbst erledigen.

Hier ist eine Möglichkeit, jede Datei in Serie mit zu lesen und zu drucken 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
}

Hier ist eine Möglichkeit (noch verwendet Array.prototype.forEach), den Inhalt von Dateien parallel zu drucken

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)
}
Richytong
quelle
Das erste Senario ist ideal für Loops, die in Serie ausgeführt werden müssen und für die Sie nicht verwenden können
Mark Odey
0

Ähnlich wie bei Antonio Val ist p-iterationein 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 verfügt es async-afüber eine statische Methode (log / logAF), die die Ergebnisse von Versprechungen 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 verketten können, um Folgendes zu tun:

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

Scott Rüdiger
quelle
0

Um zu sehen, wie das schief gehen kann, drucken Sie console.log am Ende der Methode.

Dinge, die im Allgemeinen schief gehen können:

  • Beliebige Reihenfolge.
  • printFiles kann vor dem Drucken von Dateien ausgeführt werden.
  • Schwache Leistung.

Diese sind nicht immer falsch, treten jedoch häufig in Standardanwendungsfällen auf.

Im Allgemeinen führt die Verwendung von forEach zu allen außer dem letzten. Es ruft jede Funktion auf, ohne auf die Funktion zu warten, was bedeutet, dass alle Funktionen gestartet und beendet werden, ohne auf das Beenden der Funktionen zu warten.

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

Dies ist ein Beispiel in nativem JS, das die Ordnung beibehält, verhindert, dass die Funktion vorzeitig zurückkehrt, und theoretisch die optimale Leistung beibehält.

Dieser Wille:

  • Initiieren Sie alle Dateilesevorgänge parallel.
  • Behalten Sie die Reihenfolge bei, indem Sie mithilfe der Karte Dateinamen den Versprechungen zuordnen, auf die Sie warten müssen.
  • Warten Sie auf jedes Versprechen in der vom Array festgelegten Reihenfolge.

Bei dieser Lösung wird die erste Datei angezeigt, sobald sie verfügbar ist, ohne darauf warten zu müssen, dass die anderen zuerst verfügbar sind.

Außerdem werden alle Dateien gleichzeitig geladen, anstatt warten zu müssen, bis die erste abgeschlossen ist, bevor der zweite Lesevorgang gestartet werden kann.

Der einzige Nachteil dieser und der Originalversion besteht darin, dass es schwieriger ist, Fehler zu behandeln, wenn mehrere Lesevorgänge gleichzeitig gestartet werden, da mehr Fehler gleichzeitig auftreten können.

Bei Versionen, die jeweils eine Datei lesen, wird der Fehler dann gestoppt, ohne dass Zeit damit verschwendet wird, weitere Dateien zu lesen. Selbst mit einem ausgeklügelten Stornierungssystem kann es schwierig sein, einen Fehler bei der ersten Datei zu vermeiden, aber auch die meisten anderen Dateien bereits zu lesen.

Die Leistung ist nicht immer vorhersehbar. Während viele Systeme mit parallelen Dateilesevorgängen schneller sind, bevorzugen einige sequentielle. Einige sind dynamisch und können sich unter Last verschieben. Optimierungen, die Latenz bieten, liefern bei starken Konflikten nicht immer einen guten Durchsatz.

In diesem Beispiel gibt es auch keine Fehlerbehandlung. Wenn etwas erfordert, dass sie entweder alle erfolgreich angezeigt werden oder gar nicht, wird dies nicht der Fall sein.

Es wird empfohlen, in jeder Phase eingehende Experimente mit console.log und gefälschten Lösungen zum Lesen von Dateien durchzuführen (stattdessen zufällige Verzögerung). Obwohl viele Lösungen in einfachen Fällen dasselbe zu tun scheinen, weisen alle subtile Unterschiede auf, die eine zusätzliche Prüfung erfordern, um sie herauszuquetschen.

Verwenden Sie dieses Modell, um den Unterschied zwischen Lösungen zu erkennen:

(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}`);
})();
jgmjgm
quelle
-3

Ich würde die gut getesteten (Millionen von Downloads pro Woche) Pify- und Async- Module verwenden. Wenn Sie mit dem Async-Modul nicht vertraut sind, empfehle ich Ihnen dringend, die Dokumentation zu lesen . Ich habe gesehen, dass mehrere Entwickler Zeit damit verschwenden, ihre Methoden neu zu erstellen, oder schlimmer noch, schwer zu wartenden asynchronen Code zu erstellen, wenn asynchrone Methoden höherer Ordnung den Code vereinfachen würden.

const async = require('async')
const fs = require('fs-promise')
const pify = require('pify')

async function getFilePaths() {
    return Promise.resolve([
        './package.json',
        './package-lock.json',
    ]);
}

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

  await pify(async.eachSeries)(files, async (file) => {  // <-- run in series
  // await pify(async.each)(files, async (file) => {  // <-- run in parallel
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
  console.log('HAMBONE')
}

printFiles().then(() => {
    console.log('HAMBUNNY')
})
// ORDER OF LOGS:
// package.json contents
// package-lock.json contents
// HAMBONE
// HAMBUNNY
```

Zachary Ryan Smith
quelle
Dies ist ein Schritt in die falsche Richtung. Hier ist eine Mapping-Anleitung, die ich erstellt habe, um die Leute in der modernen JS-Ära in die Hölle der Rückrufe zu bringen: github.com/jmjpro/async-package-to-async-await/blob/master/… .
jbustamovej
Wie Sie hier sehen können , bin ich an der Verwendung von async / await anstelle der async lib interessiert und offen dafür. Im Moment denke ich, dass jeder eine Zeit und einen Ort hat. Ich bin nicht davon überzeugt, dass die asynchrone lib == "Callback Hell" und async / await == "die moderne JS-Ära". imo, wenn async lib> async / await: 1. komplexer Ablauf (z. B. Warteschlange, Fracht, auch automatisch, wenn die Dinge kompliziert werden) 2. Parallelität 3. Unterstützung von Arrays / Objekten / Iterables 4. Fehlerbehandlung
Zachary Ryan Smith