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.
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)
}));
}
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
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()
Au lieu de Promise.all
en conjonction avec Array.prototype.map
(qui ne garantit pas l'ordre dans lequel les Promise
s 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());
}
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));
}
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)))
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));
}
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.
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:
Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>
.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
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
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();
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);
}
}