Utiliser async / wait avec une boucle forEach


1133

Y a-t-il des problèmes avec l'utilisation async / awaiten forEachboucle? J'essaie de parcourir un tableau de fichiers et awaitle contenu de chaque fichier.

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

Ce code fonctionne, mais quelque chose pourrait-il mal se passer avec ça? Quelqu'un m'a dit que tu n'étais pas censé utiliserasync / awaitdans une fonction d'ordre supérieur comme celle-ci, alors je voulais juste vous demander s'il y avait un problème avec cela.

Réponses:


2158

Bien sûr, le code fonctionne, mais je suis sûr qu'il ne fait pas ce que vous attendez de lui. Il déclenche simplement plusieurs appels asynchrones, mais la printFilesfonction retourne immédiatement après cela.

Lecture en séquence

Si vous souhaitez lire les fichiers en séquence, vous ne pouvez pas utiliser enforEach effet. Utilisez simplement une for … ofboucle moderne à la place, dans laquelle awaitfonctionnera comme prévu:

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

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

Lecture en parallèle

Si vous souhaitez lire les fichiers en parallèle, vous ne pouvez pas utiliser enforEach effet. Chacun des asyncappels de la fonction de rappel renvoie une promesse, mais vous les jetez au lieu de les attendre. Utilisez simplement à la mapplace, et vous pouvez attendre l'éventail de promesses que vous obtiendrez avec 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)
  }));
}

33
Pourriez-vous expliquer pourquoi cela for ... of ...fonctionne?
Demonbane

84
ok je sais pourquoi ... Utiliser Babel transformera async/ awaiten fonction de générateur et forEachcela signifie que chaque itération a une fonction de générateur individuelle, qui n'a rien à voir avec les autres. ils seront donc exécutés indépendamment et sans aucun contexte next()avec les autres. En fait, une simple for()boucle fonctionne également car les itérations sont également dans une seule fonction de générateur.
Demonbane

21
@Demonbane: En bref, parce qu'il a été conçu pour fonctionner :-) awaitsuspend l' évaluation de la fonction actuelle , y compris toutes les structures de contrôle. Oui, il est assez similaire aux générateurs à cet égard (c'est pourquoi ils sont utilisés pour polyfill asynchroniser / attendre).
Bergi

3
@ arve0 Pas vraiment, une asyncfonction est assez différente d'un Promiserappel d'exécuteur, mais oui, le maprappel renvoie une promesse dans les deux cas.
Bergi

5
Lorsque vous en apprendrez plus sur les promesses JS, mais utilisez plutôt une demi-heure de traduction en latin. J'espère que vous êtes fier @Bergi;)
Félix Gagnon-Grenier

190

Avec ES2018, vous êtes en mesure de simplifier considérablement toutes les réponses ci-dessus pour:

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

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

Voir spec: proposition-async-itération


10/09/2018: Cette réponse a suscité beaucoup d'attention récemment, veuillez consulter le billet de blog d'Axel Rauschmayer pour plus d'informations sur l'itération asynchrone: ES2018: itération asynchrone


4
Voté, ce serait bien si vous pouviez mettre un lien vers la spécification dans votre réponse pour quiconque souhaite en savoir plus sur l'itération asynchrone.
saadq

8
Ne devrait-il pas s'agir de contenu au lieu de fichier dans l'itérateur
FluffyBeing

10
Pourquoi les gens votent-ils pour cette réponse? Examinez de plus près la réponse, la question et la proposition. Après le ofdevrait être la fonction async qui retournera un tableau. Cela ne fonctionne pas et Francisco a dit;
Yevhenii Herasymchuk du

3
Entièrement d'accord avec @AntonioVal. Ce n'est pas une réponse.
Yevhenii Herasymchuk

2
Bien que je convienne que ce n'est pas une réponse, voter pour une proposition est un moyen d'augmenter sa popularité en la rendant potentiellement disponible plus tôt pour une utilisation ultérieure.
Robert Molina

62

Au lieu de Promise.allconjointement avec Array.prototype.map(qui ne garantit pas l'ordre dans lequel les Promises sont résolus), j'utilise Array.prototype.reduce, en commençant par un résolu 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());
}

1
Cela fonctionne parfaitement, merci beaucoup. Pourriez-vous expliquer ce qui se passe ici avec Promise.resolve()et await promise;?
parrker9

1
C'est plutôt cool. Ai-je raison de penser que les fichiers seront lus dans l'ordre et pas tous en même temps?
GollyJer

1
@ parrker9 Promise.resolve()renvoie un Promiseobjet déjà résolu , donc qui reducedoit Promisecommencer par. await promise;attendra que le dernier Promisede la chaîne soit résolu. @GollyJer Les fichiers seront traités séquentiellement, un à la fois.
Timothy Zorn

Utilisation très cool de réduire, merci pour le commentaire! Je signale simplement que, contrairement à certaines des autres méthodes mentionnées dans les commentaires, celle-ci est synchrone, ce qui signifie que les fichiers sont lus les uns après les autres et non en parallèle (car la prochaine itération de la fonction de réduction repose sur la précédente itération, elle doit être synchrone).
Shay Yzhakov

1
@Shay, tu veux dire séquentielle, pas synchrone. C'est toujours asynchrone - si d'autres choses sont planifiées, elles s'exécuteront entre les itérations ici.
Timothy Zorn

33

La p-itération module sur npm implémente les méthodes d'itération de tableau afin qu'elles puissent être utilisées d'une manière très simple avec async / wait.

Un exemple avec votre cas:

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

1
J'aime cela car il a les mêmes fonctions / méthodes que JS lui-même - dans mon cas, j'en avais besoin someplutôt que forEach. Merci!
mikemaccana

26

Voici quelques forEachAsyncprototypes. Notez que vous en aurez besoin 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));
}

Notez que même si vous pouvez l'inclure dans votre propre code, vous ne devez pas l'inclure dans les bibliothèques que vous distribuez à d'autres (pour éviter de polluer leurs globaux).


1
Bien que j'hésite à ajouter des éléments directement au prototype, il s'agit d'une belle mise en œuvre asynchrone pour chaque
DaniOcean

2
Tant que le nom est unique à l'avenir (comme j'utiliserais _forEachAsync), c'est raisonnable. Je pense aussi que c'est la meilleure réponse car elle enregistre beaucoup de code standard.
mikemaccana

1
@estus C'est pour éviter de polluer le code des autres. Si le code appartient à notre organisation personnelle et que les globaux sont dans un fichier bien identifié (ce globals.jsserait bien), nous pouvons ajouter des globaux comme nous le souhaitons.
mikemaccana

1
@mikemaccana C'est pour éviter les mauvaises pratiques généralement acceptées. C'est vrai, cela peut être fait tant que vous n'utilisez que du code propriétaire, ce qui arrive rarement. Le problème est que lorsque vous utilisez des bibliothèques tierces, il peut y avoir un autre type qui ressent la même chose et modifie les mêmes globaux, simplement parce que cela semblait une bonne idée au moment où une bibliothèque a été écrite.
Estus Flask du

1
@estus Bien sûr. J'ai ajouté un avertissement à la question pour enregistrer la discussion (pas particulièrement productive) ici.
mikemaccana

7

En plus de la réponse de @ Bergi , j'aimerais offrir une troisième alternative. C'est très similaire au 2ème exemple de @ Bergi, mais au lieu d'attendre chacun readFileindividuellement, vous créez un tableau de promesses, chacune que vous attendez à la fin.

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

Notez que la fonction passée à .map()n'a pas besoin d'être async, car fs.readFileretourne quand même un objet Promise. C'est donc promisesun tableau d'objets Promise, qui peut être envoyé àPromise.all() .

Dans la réponse de @ Bergi, la console peut enregistrer le contenu des fichiers dans l'ordre de leur lecture. Par exemple, si un très petit fichier termine la lecture avant un très gros fichier, il sera enregistré en premier, même si le petit fichier vient après le gros fichier du filestableau. Cependant, dans ma méthode ci-dessus, vous êtes assuré que la console enregistrera les fichiers dans le même ordre que le tableau fourni.


1
Je suis sûr que vous vous trompez: je suis sûr que votre méthode peut également lire les fichiers dans le désordre. Oui, il enregistrera la sortie dans le bon ordre (en raison de await Promise.all), mais les fichiers peuvent avoir été lus dans un ordre différent, ce qui contredit votre déclaration "vous avez la garantie que la console enregistrera les fichiers dans le même ordre qu'ils le sont" lis".
Venryx

1
@Venryx Vous avez raison, merci pour la correction. J'ai mis à jour ma réponse.
chharvey

6

La solution de Bergi fonctionne bien quand elle fsest basée sur des promesses. Vous pouvez utiliser bluebird, fs-extraoufs-promise pour cela.

Cependant, la solution pour la fsbibliothèque native du nœud est la suivante:

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

Remarque: require('fs') prend obligatoirement la fonction comme 3e argument, sinon renvoie une erreur:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

5

Les deux solutions ci-dessus fonctionnent, cependant, Antonio fait le travail avec moins de code, voici comment cela m'a aidé à résoudre les données de ma base de données, à partir de plusieurs références enfants différentes, puis à les pousser toutes dans un tableau et à les résoudre dans une promesse après tout est terminé:

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

4

il est assez facile de faire apparaître quelques méthodes dans un fichier qui gérera les données asynchrones dans un ordre sérialisé et donnera une saveur plus conventionnelle à votre code. Par exemple:

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

maintenant, en supposant que ce soit enregistré dans './myAsync.js', vous pouvez faire quelque chose de similaire à ce qui suit dans un fichier adjacent:

...
/* 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
Addendum mineur, n'oubliez pas d'envelopper vos attend / asyncs dans des blocs try / catch !!
Jay Edwards

4

Comme la réponse de @ Bergi, mais avec une différence.

Promise.all rejette toutes les promesses si l'on est rejeté.

Donc, utilisez une récursivité.

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

readFilesQueueest en dehors de la printFilescause de l'effet secondaire * introduit parconsole.log , il est préférable de se moquer, de tester et / ou d'espionner, donc ce n'est pas cool d'avoir une fonction qui renvoie le contenu (sidenote).

Par conséquent, le code peut simplement être conçu par cela: trois fonctions séparées qui sont "pures" ** et n'introduisent aucun effet secondaire, traitent la liste entière et peuvent facilement être modifiées pour gérer les cas ayant échoué.

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)

Édition future / état actuel

Le nœud prend en charge l'attente de niveau supérieur (cela n'a pas encore de plugin, ne l'aura pas et peut être activé via les indicateurs d'harmonie), c'est cool mais ne résout pas un problème (stratégiquement, je ne travaille que sur les versions LTS). Comment obtenir les fichiers?

Utilisation de la composition. Étant donné le code, cela me donne l'impression que c'est à l'intérieur d'un module, donc, devrait avoir une fonction pour le faire. Sinon, vous devez utiliser un IIFE pour envelopper le code de rôle dans une fonction asynchrone créant un module simple qui fait tout pour vous, ou vous pouvez choisir la bonne façon, il y a, la composition.

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

Notez que le nom de la variable change en raison de la sémantique. Vous passez un foncteur (une fonction qui peut être invoquée par une autre fonction) et recevez un pointeur sur la mémoire qui contient le bloc logique initial de l'application.

Mais, si ce n'est pas un module et que vous devez exporter la logique?

Enveloppez les fonctions dans une fonction asynchrone.

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

Ou changez les noms des variables, peu importe ...


* par effet secondaire menans tout effet colacteral de l'application qui peut changer le statut / comportement ou introuce des bogues dans l'application, comme IO.

** par "pure", c'est dans l'apostrophe car les fonctions ne sont pas pures et le code peut être convergé vers une version pure, quand il n'y a pas de sortie console, seulement des manipulations de données.

De plus, pour être pur, vous devrez travailler avec des monades qui gèrent les effets secondaires, qui sont sujettes aux erreurs et traitent cette erreur séparément de l'application.


3

Une mise en garde importante est la suivante: la await + for .. ofméthode et la forEach + asyncmanière ont en fait un effet différent.

Avoir à l' awaitintérieur d'une vraie forboucle s'assurera que tous les appels asynchrones sont exécutés un par un. Et la forEach + asyncfaçon de tirer toutes les promesses en même temps, ce qui est plus rapide mais parfois débordé ( si vous effectuez une requête DB ou visitez certains services Web avec des restrictions de volume et que vous ne souhaitez pas transférer 100000 appels à la fois).

Vous pouvez également utiliser reduce + promise(moins élégant) si vous ne l'utilisez pas async/awaitet que vous voulez vous assurer que les fichiers sont lus l' un après l'autre .

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

Ou vous pouvez créer un forEachAsync pour vous aider, mais utiliser essentiellement le même pour la boucle sous-jacente.

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

Jetez un œil à Comment définir la méthode en javascript sur Array.prototype et Object.prototype afin qu'elle n'apparaisse pas dans la boucle for in . Vous devriez également probablement utiliser la même itération que native forEach- accéder aux index au lieu de compter sur l'itérabilité - et passer l'index au rappel.
Bergi

Vous pouvez utiliser Array.prototype.reduced'une manière qui utilise une fonction asynchrone. J'ai montré un exemple dans ma réponse: stackoverflow.com/a/49499491/2537258
Timothy Zorn

3

En utilisant Task, futurize et une liste traversable, vous pouvez simplement faire

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

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

Voici comment procéder

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)

Une autre façon de structurer le code souhaité serait de

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

Ou peut-être encore plus orienté fonctionnellement

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

Puis à partir de la fonction parent

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

Si vous vouliez vraiment plus de flexibilité dans l'encodage, vous pouvez simplement le faire (pour le plaisir, j'utilise l' opérateur proposé Pipe Forward )

import { curry, flip } from 'ramda'

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

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

PS - Je n'ai pas essayé ce code sur la console, il pourrait y avoir des fautes de frappe ... "freestyle tout droit, en haut du dôme!" comme diraient les enfants des années 90. :-p


3

Actuellement, la propriété prototype Array.forEach ne prend pas en charge les opérations asynchrones, mais nous pouvons créer notre propre remplissage poly pour répondre à nos besoins.

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

Et c'est tout! Vous disposez désormais d'une méthode async forEach disponible sur tous les tableaux définis après ces opérations.

Testons-le ...

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

Nous pourrions faire de même pour certaines des autres fonctions du tableau comme 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

... etc :)

Quelques points à noter:

  • Votre fonction itérateur doit être une fonction asynchrone ou une promesse
  • Les tableaux créés auparavant Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>n'auront pas cette fonctionnalité disponible

3

Ajout juste à la réponse originale

  • La syntaxe de lecture parallèle dans la réponse originale est parfois déroutante et difficile à lire, peut-être pouvons-nous l'écrire dans une approche différente
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);
}
  • Pour un fonctionnement séquentiel, pas seulement pour ... de , la boucle normale pour fonctionnera également
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);
  }
}

2

Pour voir comment cela peut mal tourner, imprimez console.log à la fin de la méthode.

Choses qui peuvent mal tourner en général:

  • Ordre arbitraire.
  • printFiles peut terminer son exécution avant d'imprimer des fichiers.
  • Mauvaise performance.

Ceux-ci ne sont pas toujours faux mais sont fréquemment dans les cas d'utilisation standard.

Généralement, l'utilisation de forEach entraînera tout sauf le dernier. Il appellera chaque fonction sans attendre la fonction, ce qui signifie qu'il indique à toutes les fonctions de démarrer puis se termine sans attendre la fin des fonctions.

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

C'est un exemple en JS natif qui préservera l'ordre, empêchera la fonction de revenir prématurément et en théorie conservera des performances optimales.

Cette volonté:

  • Lancez toutes les lectures de fichiers en parallèle.
  • Préserver l'ordre via l'utilisation de la carte pour mapper les noms de fichiers aux promesses d'attendre.
  • Attendez chaque promesse dans l'ordre défini par le tableau.

Avec cette solution, le premier fichier sera affiché dès qu'il sera disponible sans avoir à attendre que les autres soient disponibles en premier.

Il chargera également tous les fichiers en même temps plutôt que d'avoir à attendre la fin du premier avant de pouvoir démarrer la lecture du deuxième fichier.

Le seul inconvénient de cela et de la version d'origine est que si plusieurs lectures sont démarrées en même temps, il est plus difficile de gérer les erreurs car il y a plus d'erreurs qui peuvent se produire à la fois.

Avec les versions qui lisent un fichier à la fois, alors s'arrêtera en cas d'échec sans perdre de temps à essayer de lire d'autres fichiers. Même avec un système d'annulation élaboré, il peut être difficile d'éviter qu'il ne tombe en panne sur le premier fichier mais qu'il lise déjà la plupart des autres fichiers également.

Les performances ne sont pas toujours prévisibles. Alors que de nombreux systèmes seront plus rapides avec des lectures de fichiers parallèles, certains préféreront séquentiels. Certains sont dynamiques et peuvent évoluer sous charge, les optimisations qui offrent une latence ne donnent pas toujours un bon débit en cas de forte contention.

Il n'y a pas non plus de gestion des erreurs dans cet exemple. Si quelque chose exige qu'ils soient tous affichés avec succès ou pas du tout, cela ne le fera pas.

Une expérimentation approfondie est recommandée avec console.log à chaque étape et de fausses solutions de lecture de fichiers (délai aléatoire à la place). Bien que de nombreuses solutions semblent faire de même dans les cas simples, toutes présentent des différences subtiles qui nécessitent un examen supplémentaire.

Utilisez cette maquette pour aider à faire la différence entre les solutions:

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

2

Aujourd'hui, je suis tombé sur plusieurs solutions pour cela. L'exécution des fonctions d'attente asynchrone dans la boucle forEach. En construisant le wrapper autour de nous, nous pouvons y arriver.

Des explications plus détaillées sur la façon dont cela fonctionne en interne, pour le forEach natif et pourquoi il n'est pas en mesure d'effectuer un appel de fonction asynchrone et d'autres détails sur les différentes méthodes sont fournies dans le lien ici

Les multiples façons dont cela peut être fait et ce sont les suivantes,

Méthode 1: utilisation du wrapper.

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

Méthode 2: utiliser la même chose qu'une fonction générique de 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');
            }
        })
      });
    };
  }

Utilisation:

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

Méthode 3:

Utilisation de Promise.all

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

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

Méthode 4: traditionnelle pour boucle ou moderne pour boucle

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

Vos méthodes 1 et 2 sont simplement des implémentations incorrectes où elles Promise.allauraient dû être utilisées - elles ne prennent en compte aucun des nombreux cas marginaux.
Bergi

@Bergi: Merci pour les commentaires valides, veuillez m'expliquer pourquoi les méthodes 1 et 2 sont incorrectes. Il sert également le but. Cela fonctionne très bien. C'est-à-dire que toutes ces méthodes sont possibles, en fonction de la situation, on peut décider d'en choisir une. J'ai l'exemple en cours d'exécution pour le même.
PranavKAndro

Il échoue sur des tableaux vides, il n'a aucune gestion d'erreur et probablement plus de problèmes. Ne réinventez pas la roue. Utilisez simplement Promise.all.
Bergi

Dans certaines conditions où ce n'est pas possible, cela sera utile. La gestion des erreurs est également effectuée par forEach api par défaut, donc aucun problème. C'est pris en charge!
PranavKAndro

Non, il n'y a pas de conditions où ce Promise.alln'est pas possible mais async/ awaitest. Et non, forEachne gère absolument aucune erreur de promesse.
Bergi

2

Cette solution est également optimisée en mémoire afin que vous puissiez l'exécuter sur 10 000 éléments de données et requêtes. Certaines des autres solutions ici planteront le serveur sur des ensembles de données volumineux.

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

Comment utiliser?

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

2

Vous pouvez utiliser Array.prototype.forEach, mais async / attendent n'est pas si compatible. En effet, la promesse renvoyée par un rappel asynchrone s'attend à être résolue, maisArray.prototype.forEach ne résout aucune promesse de l'exécution de son rappel. Ainsi, vous pouvez utiliser forEach, mais vous devrez gérer vous-même la résolution de la promesse.

Voici un moyen de lire et d'imprimer chaque fichier en série en utilisant 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
}

Voici un moyen (toujours utilisé Array.prototype.forEach) d'imprimer le contenu des fichiers en parallèle

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

Le premier senario est idéal pour les boucles qui doivent être exécutées en série et que vous ne pouvez pas utiliser pour of
Mark Odey

0

Semblable à Antonio Val p-iteration, un module npm alternatif est 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();

Alternativement, async-afa une méthode statique (log / logAF) qui enregistre les résultats des promesses:

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

Cependant, le principal avantage de la bibliothèque est que vous pouvez chaîner des méthodes asynchrones pour faire quelque chose comme:

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


-3

J'utiliserais les modules pify et async bien testés (des millions de téléchargements par semaine) . Si vous n'êtes pas familier avec le module async, je vous recommande fortement de consulter ses documents . J'ai vu plusieurs développeurs perdre du temps à recréer ses méthodes, ou pire, rendre le code asynchrone difficile à maintenir lorsque les méthodes asynchrones d'ordre supérieur simplifieraient le code.

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


C'est un pas dans la mauvaise direction. Voici un guide de mappage que j'ai créé pour aider les gens à rester coincés dans l'enfer des rappels dans l'ère JS moderne: github.com/jmjpro/async-package-to-async-await/blob/master/… .
jbustamovej

comme vous pouvez le voir ici , je suis intéressé et ouvert à utiliser async / wait au lieu de la bibliothèque async. En ce moment, je pense que chacun a un temps et un lieu. Je ne suis pas convaincu que la lib async == "enfer de rappel" et async / attendent == "l'ère JS moderne". imo, lorsque async lib> async / wait: 1. flux complexe (par exemple, file d'attente, cargaison, même auto lorsque les choses se compliquent) 2. concurrence 3. support des tableaux / objets / itérables 4. gestion des erreurs
Zachary Ryan Smith
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.