web-dev-qa-db-fra.com

Utilisation de async/wait avec une boucle forEach

L'utilisation de async/await dans une boucle forEach pose-t-elle des problèmes? J'essaie de parcourir un tableau de fichiers et await sur le 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 est-ce que quelque chose ne va pas? Quelqu'un m'a dit que tu n'étais pas supposé utiliser async/await dans une fonction d'ordre supérieur comme celle-ci, alors je voulais simplement demander s'il y avait un problème avec cela.

566
saadq

Bien sûr, le code fonctionne, mais je suis sûr qu'il ne fait pas ce que vous attendez. Elle déclenche simplement plusieurs appels asynchrones, mais la fonction printFiles est renvoyée immédiatement après cela.

Si vous voulez lire les fichiers en séquence, vous ne pouvez pas utiliser forEach. Utilisez simplement une boucle moderne for … of, dans laquelle await fonctionnera 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);
  }
}

Si vous voulez lire les fichiers en parallèle, vous ne pouvez pas utiliser forEach. Chacun des appels de la fonction de rappel async renvoie une promesse, mais vous les jetez au lieu de les attendre. Utilisez simplement map à la place, et vous pouvez attendre le tableau 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)
  }));
}
1180
Bergi

Avec ES2018, vous pouvez grandement simplifier toutes les réponses ci-dessus:

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

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

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


2018-09-10: Cette réponse a beaucoup retenu l'attention récemment. Veuillez consulter le blog d'Axel Rauschmayer pour plus d'informations sur l'itération asynchrone: http://2ality.com/2016/10/asynchronous-iteration.html

83
Francisco Mateo

Pour moi, utiliser Promise.all() avec map() est un peu difficile à comprendre et à commenter, mais si vous voulez le faire en langage clair, c'est votre meilleur plan, je suppose.

Si cela ne vous dérange pas d'ajouter un module, j'ai implémenté les méthodes d'itération Array afin qu'elles puissent être utilisées de 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);
  });
}

printFiles()

p-itération

22
Antonio Val

Au lieu de Promise.all en conjonction avec Array.prototype.map (qui ne garantit pas l'ordre dans lequel les Promises sont résolues), j'utilise Array.prototype.reduce, en commençant par un Promise résolu:

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

Voici quelques exemples de prototypes asynchrones:

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

Les 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 placer toutes dans un tableau et à les résoudre dans une promesse. 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)))
2
Hooman Askari

il est assez facile de placer deux 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 cela soit sauvegardé dans './myAsync.js', vous pouvez faire quelque chose de similaire au ci-dessous 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
Jay Edwards

En plus de la réponse de @ Bergi , je voudrais proposer une troisième alternative. Cela ressemble beaucoup au deuxième exemple de @ Bergi, mais au lieu d’attendre chaque readFile individuellement, vous créez un tableau de promesses, 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 transmise à .map() n'a pas besoin d'être async, car fs.readFile renvoie quand même un objet Promise. Par conséquent, promises est un tableau d'objets Promise qui peuvent être envoyés à Promise.all().

Dans la réponse de @ Bergi, la console peut enregistrer le contenu du fichier dans l’ordre. Par exemple, si un très petit fichier finit de lire avant un très gros fichier, il sera d'abord consigné, même si le petit fichier arrive après le gros fichier du tableau files. Cependant, dans la méthode ci-dessus, vous avez la garantie que la console enregistrera les fichiers dans le même ordre de lecture.

1
chharvey

Actuellement, la propriété prototype Array.forEach ne prend pas en charge les opérations asynchrones, mais nous pouvons créer notre propre remplissage multiple 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 maintenant d'une méthode async forEach disponible sur tous les tableaux définis après ces opérations to.

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 la même chose 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 iteratorFunction doit être une fonction asynchrone ou une promesse
  • Cette fonctionnalité n'est pas disponible pour les tableaux créés avant Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>.
1
Beau

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 vous organisez ceci

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 même 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 de la fonction parent

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

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

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, j'ai peut-être eu quelques fautes de frappe… comme diraient les enfants des années 90. :-p

1
Babakness

La solution de Bergi fonctionne bien lorsque fs est basé sur une promesse. Vous pouvez utiliser bluebird, fs-extra ou fs-promise pour cela.

Cependant, la solution pour la bibliothèque fs native de node 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 en tant que troisième argument, sinon une erreur est générée:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
0

Similaire à p-iteration d’Antonio Val, un autre module npm 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();

Sinon, async-af a 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

0
Scott Rudiger

Un caveat important est le suivant: La méthode await + for .. of et la méthode forEach + async ont en réalité un effet différent. 

Si vous avez await dans une boucle for réelle, tous les appels asynchrones seront exécutés un par un. Et la méthode forEach + async déclenchera toutes les promesses en même temps, ce qui est plus rapide mais parfois dépassé (si vous interrogez une base de données ou visitez des services Web avec des restrictions de volume et ne souhaitez pas déclencher 100 000 appels à la fois. ). 

Vous pouvez également utiliser reduce + promise (moins élégant) si vous n'utilisez pas async/await et souhaitez 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()
)

Vous pouvez également créer un fichier forEachAsync pour vous aider, mais utilisez essentiellement la même chose pour la boucle sous-jacente.

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