it-swarm-pt.tech

Usando async/wait com um loop forEach

Há algum problema com o uso de async/await em um loop forEach? Eu estou tentando percorrer uma matriz de arquivos e await no 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 algo pode dar errado com isso? Eu tinha alguém me dizendo 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.

566
saadq

Claro que o código funciona, mas tenho certeza que ele não faz o que você espera que ele faça. Ele apenas dispara várias chamadas assíncronas, mas a função printFiles retorna imediatamente após isso.

Se você quiser ler os arquivos em sequência, você não pode usar forEachde fato. Apenas use um loop for … of moderno, no qual await funcionará como esperado:

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

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

Se você quiser ler os arquivos em paralelo, você não pode usar forEachde fato. Cada uma das chamadas de função callback async retorna uma promessa, mas você as está jogando fora em vez de esperá-las. Apenas use map, e você pode esperar o conjunto de promessas que você obterá 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)
  }));
}
1180
Bergi

Com o ES2018, você pode simplificar bastante todas as respostas acima para:

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

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

Veja as especificações: https://github.com/tc39/proposal-async-iteration


2018-09-10: Esta resposta tem recebido muita atenção recentemente, por favor veja o post no blog de Axel Rauschmayer para mais informações sobre a iteração assíncrona: http://2ality.com/2016/10/asynchronous-iteration.html

83
Francisco Mateo

Para mim, usando Promise.all() com map() é um pouco difícil de entender e verboso, mas se você quiser fazê-lo em JS simples, é sua melhor chance, eu acho.

Se você não se importar em adicionar um módulo, eu implementei os métodos de iteração da Matriz para que eles possam ser usados ​​de uma forma muito direta com async/wait.

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

printFiles()

p iteração

22
Antonio Val

Em vez de Promise.all em conjunto com Array.prototype.map (que não garante a ordem em que as Promises são resolvidas), eu uso Array.prototype.reduce, começando com Promise resolvido:

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

Aqui estão alguns protótipos assíncronos forEach:

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

Ambas as soluções acima funcionam, no entanto, Antonio faz o trabalho com menos código, aqui está como ele me ajudou a resolver dados do meu banco de dados, de vários sub-filhos diferentes e, em seguida, empurrá-los todos em uma matriz e resolvê-lo em uma promessa depois de tudo 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)))
2
Hooman Askari

É muito fácil inserir alguns métodos em um arquivo que manipulará dados assíncronos em uma ordem serializada e dará 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, assumindo que é 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));
}
2
Jay Edwards

Além da resposta de @ Bergi , gostaria de oferecer uma terceira alternativa. É muito semelhante ao segundo exemplo do @ Bergi, mas em vez de aguardar cada readFile individualmente, você cria uma série de promessas, cada uma das quais você 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);
}

Note que a função passada para .map() não precisa ser async, já que fs.readFile retorna um objeto Promise mesmo assim. Portanto promises é uma matriz de objetos Promise, que podem ser enviados para Promise.all().

Na resposta do @ Bergi, o console pode registrar o conteúdo do arquivo fora de ordem. Por exemplo, se um arquivo realmente pequeno terminar de ler antes de um arquivo realmente grande, ele será registrado primeiro, mesmo que o arquivo pequeno venha após o arquivo grande na matriz files. No entanto, no meu método acima, você tem a garantia de que o console registrará os arquivos na mesma ordem em que foram lidos.

1
chharvey

Atualmente, a propriedade de protótipo Array.forEach não suporta operações assíncronas, mas podemos criar nosso próprio preenchimento de polifenóis 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 assíncrono 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 da matriz, como o mapa ...

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 para anotar:

  • Seu iteratorFunction deve ser uma função assíncrona ou promessa
  • Quaisquer matrizes criadas antes de Array.prototype.<yourAsyncFunc> = <yourAsyncFunc> não terão esse recurso disponível
1
Beau

Usando Tarefa, Futurizar 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ê configurou 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 quisesse mais flexibilidade na codificação, você poderia fazer isso (por diversão, eu estou usando o operador proposto Pipe Forward )

import { curry, flip } from 'ramda'

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

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

PS - Eu não tentei esse código no console, pode ter alguns erros de digitação ... "Freestyle direto, fora do topo da cúpula!" como as crianças dos anos 90 diriam. :-p

1
Babakness

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 nativo do nó fs é 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') compulsoriamente assume a função como 3ª argumentos, caso contrário, gera erro:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
0
myDoggyWritesCode

Semelhante a Antonio Val's p-iteration , um módulo npm alternativo é 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 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();

async-af

0
Scott Rudiger

Uma importante ressalva é: O método await + for .. of e o método forEach + async têm efeito diferente. 

Ter await dentro de um loop for real fará com que todas as chamadas assíncronas sejam executadas uma a uma. E o forEach + async caminho vai disparar todas as promessas ao mesmo tempo, que é mais rápido, mas às vezes oprimido ( se você fizer alguma consulta DB ou visitar alguns serviços web com restrições de volume e não quer disparar 100.000 chamadas de cada vez ). 

Você também pode usar reduce + promise (menos elegante) se não usar async/await e quiser ter certeza de que os arquivos foram 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 subjacente do loop.

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