¿Hay algún problema con el uso de async
/ await
en un bucle forEach
? Estoy tratando de recorrer una serie de archivos y await
el contenido de cada archivo.
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()
Este código funciona, pero ¿podría salir algo mal con esto? Alguien me dijo que se supone que no debes usar async
/ await
en una función de orden superior como esta, así que solo quería preguntar si había algún problema con esto.
Seguro que el código funciona, pero estoy bastante seguro de que no hace lo que esperas que haga. Simplemente dispara múltiples llamadas asincrónicas, pero la función printFiles
regresa inmediatamente después de eso.
Si desea leer los archivos en secuencia, no puede usar forEach
hecho. En su lugar, solo use un moderno for … of
loop, en el que await
funcionará como se esperaba:
async function printFiles () { const files = await getFilePaths(); for (const file of files) { const contents = await fs.readFile(file, 'utf8'); console.log(contents); } }
Si desea leer los archivos en paralelo, no puede usar forEach
. Cada una de las llamadas a la función de devolución de llamada async
devuelve una promesa, pero las descarta en lugar de esperarlas. Simplemente use el map
en su lugar, y puede esperar la variedad de promesas que obtendrá con 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) })); }
El módulo de iteración p en npm implementa los métodos de iteración de Array para que puedan usarse de una manera muy sencilla con async/await.
Un ejemplo con tu caso:
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); }); })();
Ambas soluciones anteriores funcionan, sin embargo, la de Antonio hace el trabajo con menos código, así es como me ayudó a resolver datos de mi base de datos, de varias referencias secundarias diferentes y luego insertarlas todas en una matriz y resolverlas en una promesa después de todo es hecho:
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)))
es bastante fácil incluir un par de métodos en un archivo que manejará datos asíncronos en un orden serializado y le dará un sabor más convencional a su código. Por ejemplo:
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; }; };
ahora, suponiendo que esté guardado en './myAsync.js', puede hacer algo similar a lo siguiente en un archivo adyacente:
... /* 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)); }
Una advertencia importante es: el método await + await + for .. of
y forEach + async
en realidad tienen un efecto diferente.
Tener await
dentro de un bucle for
real asegurará que todas las llamadas asíncronas se ejecuten una por una. Y la forma forEach + async
activará todas las promesas al mismo tiempo, lo que es más rápido pero a veces abrumado ( si realiza alguna consulta de base de datos o visita algunos servicios web con restricciones de volumen y no desea realizar 100,000 llamadas a la vez).
También puede usar reduce + promise
(menos elegante) si no usa async/await
y desea asegurarse de que los archivos se lean uno tras otro .
files.reduce((lastPromise, file) => lastPromise.then(() => fs.readFile(file, 'utf8') ), Promise.resolve() )
O puede crear un forEachAsync para ayudar, pero básicamente use el mismo for loop subyacente.
Array.prototype.forEachAsync = async function(cb){ for(let x of this){ await cb(x); } }
Estos son algunos prototipos forEachAsync
. Tenga en cuenta que tendrá que 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)); }
Tenga en cuenta que si bien puede incluir esto en su propio código, no debe incluirlo en las bibliotecas que distribuye a otros (para evitar contaminar sus globales).
En lugar de Promise.all
junto con Array.prototype.map
(que no garantiza el orden en que se resuelven las Promise
), utilizo Array.prototype.reduce
, comenzando con una Promise
resuelta:
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()); }
Usaría los módulos pify y async bien probados (millones de descargas por semana). Si no está familiarizado con el módulo asíncrono, le recomiendo que consulte sus documentos . He visto a varios desarrolladores perder el tiempo recreando sus métodos o, lo que es peor, haciendo que el código asincrónico sea difícil de mantener cuando los métodos asincrónicos de orden superior simplificarían el código.
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 ```
Además de la respuesta de @Bergi , me gustaría ofrecer una tercera alternativa. Es muy similar al segundo ejemplo de readFile
, pero en lugar de esperar cada archivo de lectura individualmente, crea una serie de promesas, cada una de las cuales espera al final.
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); }
Tenga en cuenta que la función que se pasa a .map()
no necesita ser async
, ya que fs.readFile
devuelve un objeto Promise de todos modos. Por lo tanto, promises
es una matriz de objetos Promise, que se pueden enviar a Promise.all()
.
En la respuesta de @Bergi, la consola puede registrar el contenido del archivo en el orden en que se lee. Por ejemplo, si un archivo realmente pequeño termina de leerse antes que un archivo realmente grande, se registrará primero, incluso si el archivo pequeño viene después del archivo grande en la matriz de files
. Sin embargo, en mi método anterior, tiene la garantía de que la consola registrará los archivos en el mismo orden que la matriz proporcionada.
Usando Tarea, futurizar y una Lista transitable, simplemente puede hacer
async function printFiles() { const files = await getFiles(); List(files).traverse( Task.of, f => readFile( f, 'utf-8')) .fork( console.error, console.log) }
Así es como configuraría esto
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)
Otra forma de tener estructurado el código deseado sería
const printFiles = files => List(files).traverse( Task.of, fn => readFile( fn, 'utf-8')) .fork( console.error, console.log)
O tal vez incluso más funcionalmente orientado
// 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)
Luego de la función padre
async function main() { /* awesome code with side-effects before */ printFiles( await getFiles() ); /* awesome code with side-effects after */ }
Si realmente quisiera más flexibilidad en la codificación, podría hacer esto (por diversión, estoy usando el operador Pipe Forward propuesto)
import { curry, flip } from 'ramda' export const readFile = fs.readFile |> future, |> curry, |> flip export const readFileUtf8 = readFile('utf-8')
PD: no probé este código en la consola, podría tener algunos errores tipográficos ... "¡estilo libre directo, desde la parte superior de la cúpula!" Como dirían los niños de los 90. :-pags
Actualmente, la propiedad de prototipo Array.forEach no admite operaciones asíncronas, pero podemos crear nuestro propio relleno poligonal para satisfacer nuestras necesidades.
// 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}
¡Y eso es! Ahora tiene un método async forEach disponible en cualquier matriz que se defina después de estas operaciones.
Vamos a probarlo...
// 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
Podríamos hacer lo mismo para algunas de las otras funciones de matriz como 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
... y así :)
Algunas cosas a tener en cuenta:
Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>
no tendrá esta función disponibleCon ES2018, puede simplificar en gran medida todas las respuestas anteriores a:
async function printFiles () { const files = await getFilePaths() for await (const contents of files.map(file => fs.readFile(file, 'utf8'))) { console.log(contents) } }
Ver especificación: propuesta-asincrónica-iteración
2018-09-10: esta respuesta ha recibido mucha atención recientemente, consulte la publicación de blog de Axel Rauschmayer para obtener más información sobre la iteración asíncrona.
Similar a la p-iteration
de Antonio Val, un módulo npm alternativo es 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();
Alternativamente, async-af
tiene un método estático (log/logAF) que registra los resultados de las promesas:
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();
Sin embargo, la principal ventaja de la biblioteca es que puede encadenar métodos asincrónicos para hacer algo como:
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();
Esta solución también está optimizada para memoria, por lo que puede ejecutarla en miles de elementos de datos y solicitudes. Algunas de las otras soluciones aquí bloquearán el servidor en grandes conjuntos de datos.
En mecanografiado:
export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) { for (let index = 0; index < array.length; index++) { await callback(array[index], index); } }
¿Cómo utilizar?
await asyncForEach(receipts, async (eachItem) => { await ... })
Puede usar Array.prototype.forEach
, pero async/await no es tan compatible. Esto se debe a que la promesa devuelta de una devolución de llamada asincrónica espera resolverse, pero Array.prototype.forEach
no resuelve ninguna promesa de la ejecución de su devolución de llamada. Entonces, puede usar forEach, pero tendrá que manejar la resolución de la promesa usted mismo.
Aquí hay una forma de leer e imprimir cada archivo en serie usando 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 }
Aquí hay una forma (todavía usando Array.prototype.forEach
) para imprimir el contenido de los archivos en paralelo
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) }
Solo agregando a la respuesta original
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); }
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); } }
Hoy encontré múltiples soluciones para esto. Ejecutar las funciones async await en forEach Loop. Al construir el envoltorio alrededor podemos hacer que esto suceda.
Las múltiples formas a través de las cuales se puede hacer y son las siguientes,
Método 1: Usando el envoltorio.
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étodo 2: usar lo mismo como una función genérica 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'); } }) }); }; }
uso :
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étodo 3:
Uso de Promise.all
await Promise.all(items.map(async (item) => { await someAPICall(); count++; })); console.log("count = " + count);
Método 4: bucle for tradicional o bucle for moderno
// 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);
Como la respuesta de @Bergi, pero con una diferencia.
Promise.all
rechaza todas las promesas si una es rechazada.
Entonces, usa una recursividad.
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()
PD
readFilesQueue
está fuera de printFiles
causa el efecto secundario* introducido por console.log
, es mejor simular, probar o espiar, por lo que no es bueno tener una función que devuelva el contenido (nota al margen).
Por lo tanto, el código puede diseñarse simplemente por eso: tres funciones separadas que son "puras"** y no presentan efectos secundarios, procesan la lista completa y pueden modificarse fácilmente para manejar casos fallidos.
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)
Edición futura/estado actual
Node admite la espera de nivel superior (esto aún no tiene un complemento, no lo tendrá y se puede habilitar a través de banderas de armonía), es genial pero no resuelve un problema (estratégicamente, solo trabajo en versiones LTS). ¿Cómo obtener los archivos?
Usando la composición. Dado el código, me da la sensación de que esto está dentro de un módulo, por lo que debería haber una función para hacerlo. De lo contrario, debe usar un IIFE para envolver el código de función en una función asíncrona creando un módulo simple que lo haga todo por usted, o puede ir con la composición correcta.
// more complex version with IIFE to a single module (async (files) => readFiles(await files())(getFilesPath)
Tenga en cuenta que el nombre de la variable cambia debido a la semántica. Pasas un functor (una función que puede ser invocada por otra función) y recibes un puntero en la memoria que contiene el bloque inicial de lógica de la aplicación.
Pero, si no es un módulo y necesita exportar la lógica.
Envuelva las funciones en una función asíncrona.
export const readFilesQueue = async () => { // ... to code goes here }
O cambiar los nombres de las variables, lo que sea...
*
por efecto secundario se refiere a cualquier efecto colateral de la aplicación que pueda cambiar el estado/comportamiento o introducir errores en la aplicación, como IO.
**
por "puro", está en apóstrofe ya que las funciones no son puras y el código se puede converger a una versión pura, cuando no hay salida de consola, solo manipulaciones de datos.
Aparte de esto, para ser puro, deberá trabajar con mónadas que manejen el efecto secundario, que son propensas a errores y traten ese error por separado de la aplicación.
Para ver cómo puede salir mal, imprime console.log al final del método.
Cosas que pueden salir mal en general:
Estos no siempre son incorrectos, pero con frecuencia se encuentran en casos de uso estándar.
En general, el uso de forEach dará como resultado todos menos el último. Llamará a cada función sin esperar la función, lo que significa que le dice a todas las funciones que comiencen y luego finalice sin esperar a que finalicen las funciones.
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()
Este es un ejemplo en JS nativo que preservará el orden, evitará que la función regrese prematuramente y, en teoría, mantendrá un rendimiento óptimo.
Esta voluntad:
Con esta solución, el primer archivo se mostrará tan pronto como esté disponible sin tener que esperar a que los demás estén disponibles primero.
También cargará todos los archivos al mismo tiempo en lugar de tener que esperar a que termine el primero antes de poder iniciar la lectura del segundo archivo.
El único inconveniente de esto y la versión original es que si se inician varias lecturas a la vez, es más difícil manejar los errores debido a que pueden ocurrir más errores a la vez.
Con las versiones que leen un archivo a la vez, se detendrán en caso de falla sin perder tiempo tratando de leer más archivos. Incluso con un sistema de cancelación elaborado, puede ser difícil evitar que falle en el primer archivo, pero que también lea la mayoría de los otros archivos.
El rendimiento no siempre es predecible. Si bien muchos sistemas serán más rápidos con lecturas de archivos paralelas, algunos preferirán secuencial. Algunos son dinámicos y pueden cambiar bajo carga, las optimizaciones que ofrecen latencia no siempre producen un buen rendimiento bajo una fuerte contención.
Tampoco hay manejo de errores en ese ejemplo. Si algo requiere que se muestren todos con éxito o que no se muestren en absoluto, no lo hará.
Se recomienda experimentar en profundidad con console.log en cada etapa y soluciones de lectura de archivos falsos (retraso aleatorio en su lugar). Aunque muchas soluciones parecen hacer lo mismo en casos simples, todas tienen diferencias sutiles que requieren un escrutinio adicional para eliminarlas.
Use este simulacro para ayudar a distinguir la diferencia entre las soluciones:
(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}`); })();
La solución de Bergi funciona bien cuando fs
se basa en promesas. Puede usar bluebird
, fs-extra
o fs-promise
para esto.
Sin embargo, la solución para la biblioteca fs
nativa del nodo es la siguiente:
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; }));
Nota: require('fs')
obligatoriamente toma la función como tercer argumento, de lo contrario arroja un error:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
Si desea iterar sobre todos los elementos al mismo tiempo:
async function asyncForEach(arr, fn) { await Promise.all(arr.map(fn)); }
Si desea iterar sobre todos los elementos de forma no concurrente (por ejemplo, cuando su función de mapeo tiene efectos secundarios o ejecutar el mapeador sobre todos los elementos de la matriz a la vez sería demasiado costoso en recursos):
Opción A: Promesas
function asyncForEachStrict(arr, fn) { return new Promise((resolve) => { arr.reduce( (promise, cur, idx) => promise .then(() => fn(cur, idx, arr)), Promise.resolve(), ).then(() => resolve()); }); }
Opción B: asíncrono/espera
async function asyncForEachStrict(arr, fn) { for (let idx = 0; idx < arr.length; idx += 1) { const cur = arr[idx]; await fn(cur, idx, arr); } }
Escriba su propio asyncForEach
async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) } }
Puedes usarlo así
await asyncForEach(array, async function(item,index,array){ //await here } )
Como han mencionado otras respuestas, probablemente desee que se ejecute en secuencia en lugar de en paralelo. Es decir. ejecute para el primer archivo, espere hasta que termine, luego , una vez que haya terminado, ejecute para el segundo archivo. Eso no es lo que sucederá.
Creo que es importante abordar por qué esto no sucede.
Piensa en cómo funciona forEach
. No puedo encontrar la fuente, pero supongo que funciona de la siguiente manera:
const forEach = (arr, cb) => { for (let i = 0; i < arr.length; i++) { cb(arr[i]); } };
Ahora piensa en lo que sucede cuando haces algo como esto:
forEach(files, async logFile(file) { const contents = await fs.readFile(file, 'utf8'); console.log(contents); });
Dentro del bucle for
de forEach
estamos llamando a cb(arr[i])
, que termina siendo logFile(file)
. La función logFile
tiene una await
dentro de ella, por lo que tal vez el ciclo for
esperará esta await
antes de continuar con i++
.
No, no lo hará. Confusamente, no es así como funciona await
. De los documentos :
Una espera divide el flujo de ejecución, lo que permite que la persona que llama a la función asíncrona reanude la ejecución. Después de que await difiere la continuación de la función asíncrona, se produce la ejecución de declaraciones posteriores. Si esta espera es la última expresión ejecutada por su función, la ejecución continúa devolviendo a la persona que llama a la función una Promesa pendiente para completar la función de espera y reanudar la ejecución de esa persona que llama.
Entonces, si tiene lo siguiente, los números no se registrarán antes de "b"
:
const delay = (ms) => { return new Promise((resolve) => { setTimeout(resolve, ms); }); }; const logNumbers = async () => { console.log(1); await delay(2000); console.log(2); await delay(2000); console.log(3); }; const main = () => { console.log("a"); logNumbers(); console.log("b"); }; main();
Volviendo a forEach
, forEach
es como main
y logFile
es como logNumbers
. main
no se detendrá solo porque logNumbers
await
, y forEach
no se detendrá solo porque logFile
await
.
Una solución sencilla para reemplazar un ciclo de espera forEach()
que no funciona es reemplazar forEach
con map
y agregar Promise.all(
al principio.
Por ejemplo:
await y.forEach(async (x) => {
para
await Promise.all(y.map(async (x) => {
Se necesita un extra )
al final.
No es bueno llamar a un método asíncrono desde un bucle. Esto se debe a que cada iteración de bucle se retrasará hasta que se complete toda la operación asíncrona. Eso no es muy eficaz. También evita las ventajas de la paralelización de async
/ await
.
Una mejor solución sería crear todas las promesas a la vez y luego obtener acceso a los resultados usando Promise.all()
. De lo contrario, cada operación sucesiva no comenzará hasta que la anterior haya finalizado.
En consecuencia, el código se puede refactorizar de la siguiente manera;
const printFiles = async () => { const files = await getFilePaths(); const results = []; files.forEach((file) => { results.push(fs.readFile(file, 'utf8')); }); const contents = await Promise.all(results); console.log(contents); }
Antecedentes : Anoche estuve en una situación similar. Usé la función asíncrona como argumento foreach. El resultado fue impredecible. Cuando probé mi código 3 veces, se ejecutó sin problemas 2 veces y falló 1 vez. (algo raro)
Finalmente entendí mi cabeza e hice algunas pruebas de bloc de notas.
const getPromise = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`Promise resolved for ${time}s`) }, time) }) } const main = async () => { const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)] console.log('Before For Each Loop') myPromiseArray.forEach(async (element, index) => { let result = await element; console.log(result); }) console.log('After For Each Loop') } main();
for - of
loop como @Bergi sugirió anteriormente const getPromise = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`Promise resolved for ${time}s`) }, time) }) } const main = async () => { const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)] console.log('Before For Each Loop') // AVOID USING THIS // myPromiseArray.forEach(async (element, index) => { // let result = await element; // console.log(result); // }) // This works well for (const element of myPromiseArray) { let result = await element; console.log(result) } console.log('After For Each Loop') } main();
Si eres un poco de la vieja escuela como yo, simplemente puedes usar el bucle for clásico, que también funciona :)
const getPromise = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(`Promise resolved for ${time}s`) }, time) }) } const main = async () => { const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)] console.log('Before For Each Loop') // AVOID USING THIS // myPromiseArray.forEach(async (element, index) => { // let result = await element; // console.log(result); // }) // This works well too - the classic for loop :) for (let i = 0; i < myPromiseArray.length; i++) { const result = await myPromiseArray[i]; console.log(result); } console.log('After For Each Loop') } main();
Espero que esto ayude a alguien, buen día, saludos!
Si no puede usar async/await (IE11, old packer, etc.), puede probar esta función recursiva. fetch
como mi llamada asíncrona, pero podría usar cualquier función que devuelva una promesa.
var urlsToGet = ['https://google.com', 'https://yahoo.com']; fetchOneAtATime(urlsToGet); function fetchOneAtATime(urls) { if (urls.length === 0) { return; } fetch(urls[0]).finally(() => fetchOneAtATime(urls.slice(1))); }
Puede usar el bucle async.forEach del paquete async:
async.forEach(dataToLoop(array), async(data, cb) => { variable = await MongoQuery; }, function(err) { console.log(err); }) }) .catch((err)=>{ console.log(err); })
@Bergi ya ha dado la respuesta sobre cómo manejar este caso en particular correctamente. No duplicaré aquí.
Me gustaría abordar la diferencia entre usar forEach
y for
loop cuando se trata de async
y await
forEach
funciona cada uno
Veamos cómo funciona forEach
. De acuerdo con la especificación ECMAScript , MDN proporciona una implementación que se puede usar como polyfill. Lo copio y lo pego aquí con eliminación de comentarios.
Array.prototype.forEach = function (callback, thisArg) { if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); } var T, k; var O = Object(this); var len = O.length >>> 0; if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); } if (arguments.length > 1) { T = thisArg; } k = 0; while (k < len) { var kValue; if (k in O) { kValue = O[k]; callback.call(T, kValue, k, O); // pay attention to this line } k++; } };
Volvamos a su código, extraigamos la devolución de llamada como una función.
async function callback(file){ const contents = await fs.readFile(file, 'utf8') console.log(contents) }
Entonces, básicamente, la callback
de llamada devuelve una promesa ya que se declara con async
. Dentro de forEach
, la callback
de llamada simplemente se llama de manera normal, si la devolución de llamada en sí misma devuelve una promesa, el motor de JavaScript no esperará a que se resuelva o rechace. En su lugar, coloca la promise
en una cola de trabajos y continúa ejecutando el bucle.
¿Qué await fs.readFile(file, 'utf8')
dentro de la callback
de llamada?
Básicamente, cuando su callback
de llamada asíncrona tiene la oportunidad de ejecutarse, el motor js se detendrá hasta fs.readFile(file, 'utf8')
se resuelva o rechace, y reanudará la ejecución de la función asíncrona después del cumplimiento. Entonces, la variable de contents
almacena el resultado real de fs.readFile
, no una promise
. Entonces, console.log(contents)
cierra la sesión del contenido del archivo, no una Promise
¿Por qué for ... of
obras?
cuando escribimos un bucle for of
genérico, ganamos más control que forEach
. Vamos a refactorizar printFiles
.
async function printFiles () { const files = await getFilePaths() // Assume this works fine for (const file of files) { const contents = await fs.readFile(file, 'utf8') console.log(contents) // or await callback(file) } }
Cuando se evalúa for
loop, tenemos una promesa de await
dentro de la función async
, la ejecución se detendrá hasta que se resuelva la promesa de await
. Entonces, puedes pensar que los archivos se leen uno por uno en un orden determinado.
Ejecutar secuencialmente
A veces, realmente necesitamos que las funciones asíncronas se ejecuten en orden secuencial. Por ejemplo, tengo algunos registros nuevos almacenados en una matriz para guardarlos en la base de datos, y quiero que se guarden en orden secuencial, lo que significa que el primer registro de la matriz debe guardarse primero, luego el segundo, hasta que se guarde el último.
Aquí hay un ejemplo:
const records = [1, 2, 3, 4]; async function saveRecord(record) { return new Promise((resolved, rejected) => { setTimeout(()=> { resolved(`record ${record} saved`) }, Math.random() * 500) }); } async function forEachSaveRecords(records) { records.forEach(async (record) => { const res = await saveRecord(record); console.log(res); }) } async function forofSaveRecords(records) { for (const record of records) { const res = await saveRecord(record); console.log(res); } } (async () => { console.log("=== for of save records ===") await forofSaveRecords(records) console.log("=== forEach save records ===") await forEachSaveRecords(records) })()
Uso setTimeout
para simular el proceso de guardar un registro en la base de datos: es asíncrono y cuesta un tiempo aleatorio. Usando forEach
, los registros se guardan en un orden indeterminado, pero usando for..of
, se guardan secuencialmente.
La pregunta original del OP
¿Hay algún problema con el uso de async/await en un bucle forEach? ...
se cubrió hasta cierto punto en la respuesta seleccionada de @Bergi, que mostró cómo procesar en serie y en paralelo. Sin embargo, hay otros problemas observados con el paralelismo:
Por ejemplo, si un archivo realmente pequeño termina de leerse antes que un archivo realmente grande, se registrará primero, incluso si el archivo pequeño viene después del archivo grande en la matriz de archivos.
Tampoco es bueno abrir miles de archivos a la vez para leerlos al mismo tiempo. Uno siempre tiene que evaluar si es mejor un enfoque secuencial, paralelo o mixto.
Entonces, abordemos estos problemas mostrando un código real que sea breve y conciso, y que no use bibliotecas de terceros. Algo fácil de cortar, pegar y modificar.
La mejora más fácil es realizar un paralelismo completo como en la respuesta de @Bergi , pero hacer un pequeño cambio para que cada archivo se imprima lo antes posible mientras se conserva el orden .
async function printFiles2() { const readProms = (await getFilePaths()).map((file) => fs.readFile(file, "utf8") ); await Promise.all([ await Promise.all(readProms), // branch 1 (async () => { // branch 2 for (const p of readProms) console.log(await p); })(), ]); }
Arriba, dos sucursales separadas se ejecutan simultáneamente.
Eso fue fácil.
Un "límite de concurrencia" significa que no se leerán más de N
archivos al mismo tiempo.
Como una tienda que solo permite la entrada de tantos clientes a la vez (al menos durante COVID).
Primero se introduce una función auxiliar:
function bootablePromise(kickMe: () => Promise<any>) { let resolve: (value: unknown) => void = () => {}; const promise = new Promise((res) => { resolve = res; }); const boot = () => { resolve(kickMe()); }; return { promise, boot }; }
La función bootablePromise(kickMe:() => Promise<any>)
toma una función kickMe
como argumento para iniciar una tarea (en nuestro caso readFile
). Pero no se inicia inmediatamente.
bootablePromise
devuelve un par de propiedades
promise
de tipo Promise
boot
de tipo función ()=>void
promise
tiene dos etapas en la vida.
promise
transiciones del primer estado al segundo cuando se llama a boot()
.
bootablePromise
se usa en printFiles
--
async function printFiles4() { const files = await getFilePaths(); const boots: (() => void)[] = []; const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>(); const bootableProms = files.map((file,pidx) => { const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8")); boots.push(boot); set.add(promise.then(() => ({ pidx }))); return promise; }); const concurLimit = 2; await Promise.all([ (async () => { // branch 1 let idx = 0; boots.slice(0, concurLimit).forEach((b) => { b(); idx++; }); while (idx<boots.length) { const { pidx } = await Promise.race([...set]); set.delete([...set][pidx]); boots[idx++](); } })(), (async () => { // branch 2 for (const p of bootableProms) console.log(await p); })(), ]); }
Como antes hay dos ramas.
La diferencia ahora es que las promesas nunca más que concurLimit
pueden ejecutarse simultáneamente.
Las variables importantes son
boots
: la matriz de funciones a llamar para forzar su correspondiente promesa de transición. Se utiliza sólo en la rama 1.set
: Hay promesas en un contenedor de acceso aleatorio para que puedan eliminarse fácilmente una vez cumplidas. Este contenedor se utiliza solo en la rama 1.bootableProms
: Estas son las promesas smae como inicialmente en set
, pero es una matriz, no un conjunto, y la matriz nunca se cambia. Se utiliza sólo en la rama 2. Ejecutándose con un fs.readFile
simulado que toma tiempos de la siguiente manera (nombre de archivo versus tiempo en ms).
const timeTable = { "1": 600, "2": 500, "3": 400, "4": 300, "5": 200, "6": 100, };
se ven tiempos de ejecución de prueba como este, lo que muestra que la concurrencia está funcionando --
[1]0--0.601 [2]0--0.502 [3]0.503--0.904 [4]0.608--0.908 [5]0.905--1.105 [6]0.905--1.005
Disponible como ejecutable en el sandbox del patio de juegos mecanografiado
Esto no usa async/await como solicitó el OP y solo funciona si está en el back-end con NodeJS. Aunque todavía puede ser útil para algunas personas, porque el ejemplo dado por OP es leer el contenido del archivo, y normalmente lo hace en el backend.
Totalmente asíncrono y sin bloqueo:
const fs = require("fs") const async = require("async") const obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"} const configs = {} async.forEachOf(obj, (value, key, callback) => { fs.readFile(__dirname + value, "utf8", (err, data) => { if (err) return callback(err) try { configs[key] = JSON.parse(data); } catch (e) { return callback(e) } callback() }); }, err => { if (err) console.error(err.message) // configs is now a map of JSON data doSomethingWith(configs) })
files.forEach(async (file) => { const contents = await fs.readFile(file, 'utf8') })
El problema es que forEach()
ignora la promesa devuelta por la función de iteración. Como resultado, todas las funciones fs.readFile
se invocan en la misma ronda del bucle de eventos, lo que significa que se inician en paralelo, no en secuencia, y la ejecución continúa inmediatamente después de invocar forEach(), sin esperar a que se completen todas las funciones fs.readFile
. fs.readFile
operaciones para completar. Dado que forEach no espera a que se resuelva cada promesa, el ciclo en realidad termina de iterar antes de que se resuelvan las promesas. Puede terminar intentando acceder a valores que aún no están disponibles.