Usando async / await com um loop forEach
Há algum problema com o uso de async
/ await
em um forEach
loop? Estou tentando percorrer uma série de arquivos e await
o conteúdo de cada arquivo.
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, mas pode haver algo errado com isso? Alguém me disse que você não deveria usar async
/ await
em uma função de ordem superior como esta, então eu só queria perguntar se havia algum problema com isso.
Respostas
Claro que o código funciona, mas tenho quase certeza de que não faz o que você espera. Ele apenas dispara várias chamadas assíncronas, mas a printFiles
função retorna imediatamente depois disso.
Leitura em sequência
Se você quiser ler os arquivos em sequência, não poderá usarforEach
. Basta usar um for … of
loop moderno , no qual await
funcionará conforme o esperado:
async function printFiles () {
const files = await getFilePaths();
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
console.log(contents);
}
}
Lendo em paralelo
Se você quiser ler os arquivos em paralelo, não poderá usá-los deforEach
fato. Cada uma das async
chamadas de função de retorno de chamada retorna uma promessa, mas você as está descartando em vez de aguardá-las. Em map
vez disso, use e você poderá aguardar a série de promessas que receberá com 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)
}));
}
Com ES2018, você é capaz de simplificar muito todas as respostas acima para:
async function printFiles () {
const files = await getFilePaths()
for await (const contents of fs.readFile(file, 'utf8')) {
console.log(contents)
}
}
Veja as especificações: proposta-async-iteration
10/09/2018: Esta resposta tem recebido muita atenção recentemente, consulte a postagem do blog de Axel Rauschmayer para obter mais informações sobre iteração assíncrona: ES2018: iteração assíncrona
Em vez de Promise.all
em conjunto com Array.prototype.map
(o que não garante a ordem em que os Promise
s são resolvidos), uso Array.prototype.reduce
, começando com um resolvido 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());
}
O módulo p-iteration no npm implementa os métodos de iteração Array para que eles possam ser usados de uma maneira muito direta com async / await.
Um exemplo com o seu 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);
});
})();
Aqui estão alguns forEachAsync
protótipos. Observe que você precisará await
deles:
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));
}
Observe que, embora você possa incluir isso em seu próprio código, você não deve incluí-lo em bibliotecas distribuídas para outras pessoas (para evitar poluir seus globais).
Além da resposta de @ Bergi , gostaria de oferecer uma terceira alternativa. É muito semelhante ao segundo exemplo de @ Bergi, mas em vez de esperar cada uma readFile
individualmente, você cria uma série de promessas, cada uma das quais espera no 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);
}
Observe que a função passada para .map()
não precisa ser async
, pois fs.readFile
retorna um objeto Promise de qualquer maneira. Portanto, promises
é uma matriz de objetos Promise, que podem ser enviados para Promise.all()
.
Na resposta de @ Bergi, o console pode registrar o conteúdo do arquivo na ordem em que são lidos. Por exemplo, se um arquivo muito pequeno terminar a leitura antes de um arquivo muito grande, ele será registrado primeiro, mesmo se o arquivo pequeno vier depois do arquivo grande no files
array. No entanto, no meu método acima, você tem a garantia de que o console registrará os arquivos na mesma ordem do array fornecido.
A solução de Bergi funciona bem quando fs
é baseada em promessas. Você pode usar bluebird
, fs-extra
ou fs-promise
para isso.
No entanto, a solução para a fs
biblioteca nativa do nó é a seguinte:
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')
obrigatoriamente assume a função como terceiro argumento, caso contrário, gera erro:
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
Ambas as soluções acima funcionam, no entanto, Antonio faz o trabalho com menos código, aqui está como ele me ajudou a resolver os dados do meu banco de dados, de vários refs filhos diferentes e, em seguida, empurrando todos em um array e resolvendo em uma promessa, feito:
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)))
é muito fácil inserir alguns métodos em um arquivo que manipulará dados assíncronos em uma ordem serializada e fornecerá um sabor mais convencional ao seu código. Por exemplo:
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;
};
};
agora, supondo que esteja salvo em './myAsync.js', você pode fazer algo semelhante ao abaixo em um arquivo adjacente:
...
/* 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));
}
Esta solução também é otimizada para memória para que você possa executá-la em 10.000 itens de dados e solicitações. Algumas das outras soluções aqui irão travar o servidor em grandes conjuntos de dados.
Em 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);
}
}
Como usar?
await asyncForEach(receipts, async (eachItem) => {
await ...
})
Uma advertência importante é: o await + for .. of
método e a forEach + async
maneira na verdade têm efeitos diferentes.
Ter await
dentro de um for
loop real garantirá que todas as chamadas assíncronas sejam executadas uma por uma. E o forEach + async
caminho vai disparar todas as promessas ao mesmo tempo, o que é mais rápido, mas às vezes sobrecarregado ( se você fizer alguma consulta no banco de dados ou visitar alguns serviços da web com restrições de volume e não quiser disparar 100.000 chamadas de uma vez).
Você também pode usar reduce + promise
(menos elegante) se não usar async/await
e quiser garantir que os arquivos sejam lidos um após o outro .
files.reduce((lastPromise, file) =>
lastPromise.then(() =>
fs.readFile(file, 'utf8')
), Promise.resolve()
)
Ou você pode criar um forEachAsync para ajudar, mas basicamente usar o mesmo for loop subjacente.
Array.prototype.forEachAsync = async function(cb){
for(let x of this){
await cb(x);
}
}
Apenas adicionando à resposta original
- A sintaxe de leitura paralela na resposta original às vezes é confusa e difícil de ler, talvez possamos escrevê-la em uma abordagem diferente
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);
}
- Para operação sequencial, não apenas para ... de , o loop normal para também funcionará
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);
}
}
Como a resposta de @ Bergi, mas com uma diferença.
Promise.all
rejeita todas as promessas se uma for rejeitada.
Portanto, use uma recursão.
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
readFilesQueue
está fora da printFiles
causa o efeito colateral * introduzido por console.log
, é melhor simular, testar e / ou espiar, portanto, não é legal ter uma função que retorna o conteúdo (nota lateral).
Portanto, o código pode ser simplesmente projetado por isso: três funções separadas que são "puras" ** e não apresentam efeitos colaterais, processam a lista inteira e podem ser facilmente modificadas para lidar com casos de falha.
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)
Edição futura / estado atual
O Node suporta await de nível superior (isso ainda não tem um plugin, não terá e pode ser habilitado por meio de flags de harmonia), é legal, mas não resolve um problema (estrategicamente, trabalho apenas em versões LTS). Como obter os arquivos?
Usando composição. Dado o código, me dá a sensação de que ele está dentro de um módulo, portanto, deveria ter uma função para fazê-lo. Caso contrário, você deve usar um IIFE para envolver o código de função em uma função assíncrona, criando um módulo simples que faz tudo para você, ou você pode seguir o caminho certo, há, composição.
// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)
Observe que o nome da variável muda devido à semântica. Você passa um functor (uma função que pode ser invocada por outra função) e recebe um ponteiro na memória que contém o bloco inicial de lógica da aplicação.
Mas, se não for um módulo e você precisa exportar a lógica?
Envolva as funções em uma função assíncrona.
export const readFilesQueue = async () => {
// ... to code goes here
}
Ou mude os nomes das variáveis, o que for ...
*
por efeito colateral significa qualquer efeito colacteral do aplicativo que pode alterar o estado / comportamento ou introduzir bugs no aplicativo, como IO.
**
por "puro", está em apóstrofo, pois as funções não são puras e o código pode ser convergido para uma versão pura, quando não há saída do console, apenas manipulação de dados.
Além disso, para ser puro, você precisará trabalhar com mônadas que tratam do efeito colateral, que são propensas a erros, e tratam esse erro separadamente do aplicativo.
Usando Task, futurize e uma lista percorrível, você pode simplesmente fazer
async function printFiles() {
const files = await getFiles();
List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
.fork( console.error, console.log)
}
Aqui está como você configuraria isso
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)
Outra forma de estruturar o código desejado seria
const printFiles = files =>
List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
.fork( console.error, console.log)
Ou talvez ainda mais 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)
Então, da função pai
async function main() {
/* awesome code with side-effects before */
printFiles( await getFiles() );
/* awesome code with side-effects after */
}
Se você realmente quiser mais flexibilidade na codificação, pode simplesmente fazer isso (para se divertir, estou usando o operador Pipe Forward proposto )
import { curry, flip } from 'ramda'
export const readFile = fs.readFile
|> future,
|> curry,
|> flip
export const readFileUtf8 = readFile('utf-8')
PS - Eu não tentei este código no console, pode haver alguns erros de digitação ... "estilo livre direto, fora do topo da cúpula!" como diriam as crianças dos anos 90. :-p
Atualmente, a propriedade de protótipo Array.forEach não oferece suporte a operações assíncronas, mas podemos criar nosso próprio poli-preenchimento para atender às nossas necessidades.
// 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}
E é isso! Agora você tem um método async forEach disponível em quaisquer matrizes definidas após essas operações.
Vamos testar ...
// 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
Poderíamos fazer o mesmo para algumas das outras funções de array, 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
... e assim por diante :)
Algumas coisas a serem observadas:
- Seu iteratorFunction deve ser uma função assíncrona ou promessa
- Todas as matrizes criadas antes
Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>
não terão esse recurso disponível
Hoje me deparei com várias soluções para isso. Executando as funções async await no forEach Loop. Construindo o invólucro, podemos fazer isso acontecer.
As várias maneiras pelas quais isso pode ser feito e são as seguintes,
Método 1: usando o invólucro.
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: usando o mesmo que uma função 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:
Usando Promise.all
await Promise.all(items.map(async (item) => {
await someAPICall();
count++;
}));
console.log("count = " + count);
Método 4: loop for tradicional ou loop 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);
Você pode usar Array.prototype.forEach
, mas async / await não é tão compatível. Isso ocorre porque a promessa retornada de um retorno de chamada assíncrono espera ser resolvida, mas Array.prototype.forEach
não resolve nenhuma promessa da execução de seu retorno de chamada. Então, você pode usar forEach, mas terá que lidar com a resolução da promessa sozinho.
Aqui está uma maneira de ler e imprimir cada arquivo em série 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
}
Aqui está uma maneira (ainda usando Array.prototype.forEach
) para imprimir o conteúdo dos arquivos em 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)
}
Para ver como isso pode dar errado, imprima console.log no final do método.
Coisas que podem dar errado em geral:
- Ordem arbitrária.
- printFiles pode terminar a execução antes de imprimir os arquivos.
- Desempenho ruim.
Nem sempre estão errados, mas frequentemente estão em casos de uso padrão.
Geralmente, o uso de forEach resultará em todos, exceto no último. Ele chamará cada função sem esperar pela função, o que significa que diz a todas as funções para iniciar e termina sem esperar que as funções terminem.
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 é um exemplo em JS nativo que preservará a ordem, evitará que a função retorne prematuramente e, em teoria, manterá o desempenho ideal.
Isso vai:
- Inicie todas as leituras de arquivo para acontecer em paralelo.
- Preserve a ordem por meio do uso de mapa para mapear nomes de arquivo para promessas a aguardar.
- Aguarde cada promessa na ordem definida pela matriz.
Com esta solução, o primeiro arquivo será mostrado assim que estiver disponível, sem ter que esperar que os outros estejam disponíveis primeiro.
Ele também carregará todos os arquivos ao mesmo tempo, em vez de ter que esperar o primeiro terminar antes que a segunda leitura de arquivo possa ser iniciada.
A única desvantagem disso e da versão original é que, se várias leituras forem iniciadas ao mesmo tempo, será mais difícil lidar com os erros, pois há mais erros que podem ocorrer de uma vez.
Com versões que leem um arquivo por vez, então irão parar em caso de falha, sem perder tempo tentando ler mais nenhum arquivo. Mesmo com um sistema de cancelamento elaborado, pode ser difícil evitar que ele falhe no primeiro arquivo, mas também já lê a maioria dos outros arquivos.
O desempenho nem sempre é previsível. Embora muitos sistemas sejam mais rápidos com leituras de arquivos paralelas, alguns preferem sequencial. Alguns são dinâmicos e podem mudar sob carga, otimizações que oferecem latência nem sempre rendem um bom rendimento sob forte contenção.
Também não há tratamento de erros nesse exemplo. Se algo exige que todos eles sejam mostrados com sucesso ou não, isso não acontecerá.
Recomenda-se uma experimentação profunda com console.log em cada estágio e soluções de leitura de arquivos falsos (em vez disso, atraso aleatório). Embora muitas soluções pareçam fazer o mesmo em casos simples, todas têm diferenças sutis que exigem um exame mais minucioso para serem eliminadas.
Use esta simulação para ajudar a diferenciar as soluções:
(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}`);
})();
Semelhante ao de Antonio Val p-iteration
, um módulo alternativo de npm é 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();
Como alternativa, async-af
tem um método estático (log / logAF) que registra os resultados das promessas:
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();
No entanto, a principal vantagem da biblioteca é que você pode encadear métodos assíncronos para fazer 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();