Używanie async / wait z pętlą forEach

Czy są jakieś problemy z używaniem async/await W pętli forEach? Próbuję zapętlić tablicę plików i await Na zawartości KAŻDEGO pliku.

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()

Ten kod działa, ale czy coś może pójść nie tak? Ktoś mi powiedział, że nie powinieneś używać async/await w funkcji wyższego rzędu, takiej jak ta, więc chciałem zapytać, czy nie ma z tym problemu.

Author: saadq, 2016-06-01

12 answers

Jasne, że kod działa, ale jestem prawie pewien, że nie robi tego, czego oczekujesz. Po prostu odpala wiele wywołań asynchronicznych, ale funkcja printFiles natychmiast powraca po tym.

Jeśli chcesz czytać pliki w kolejności, nie możesz użyć forEach w rzeczy samej. Po prostu użyj nowoczesnej pętli for … of, w której await będzie działać zgodnie z oczekiwaniami:

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

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

Jeśli chcesz czytać pliki równolegle, nie możesz użyć forEach w rzeczy samej. Każdy z async wywołania funkcji zwrotnej zwracają obietnicę, ale wyrzucasz je zamiast czekać na nie. Po prostu użyj map zamiast tego, a możesz czekać na tablicę obietnic, które otrzymasz z 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)
  }));
}
 908
Author: Bergi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-01 21:50:14

Z ES2018, jesteś w stanie znacznie uprościć wszystkie powyższe odpowiedzi na:

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

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

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


2018-09-10: ta odpowiedź cieszy się ostatnio dużym zainteresowaniem, więcej informacji na temat iteracji asynchronicznej można znaleźć w poście Axela Rauschmayera: http://2ality.com/2016/10/asynchronous-iteration.html

 31
Author: Francisco Mateo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-10 20:58:29

Dla mnie używanie Promise.all() z map() jest trochę trudne do zrozumienia i słowne, ale jeśli chcesz to zrobić w zwykłym JS, to chyba najlepszy strzał.

Jeśli nie masz nic przeciwko dodaniu modułu, zaimplementowałem metody iteracji tablicy, aby można było ich używać w bardzo prosty sposób z async/wait.

Przykład z twoim przypadkiem:

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-iteracja

 15
Author: Antonio Val,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-16 00:03:01

Oto kilka prototypów foreach async:

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));
}
 9
Author: Matt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-22 15:11:16

Zamiast Promise.all w połączeniu z Array.prototype.map (co nie gwarantuje kolejności, w jakiej PromiseS są rozwiązane), używam Array.prototype.reduce, zaczynając od rozwiązanego 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());
}
 9
Author: Timothy Zorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-17 15:01:32

Oba powyższe rozwiązania działają, jednak Antonio robi to z mniejszą ilością kodu, oto jak pomogło mi rozwiązać dane z mojej bazy danych, z kilku różnych referencji potomnych, a następnie wcisnąć je wszystkie do tablicy i rozwiązać je w obietnicy po wszystkim jest zrobione: {]}

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
Author: Human Askari,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-26 10:47:21

To dość bezbolesne wstawić kilka metod do pliku, który będzie obsługiwał dane asynchroniczne w serializowanej kolejności i nada bardziej konwencjonalny smak twojemu kodowi. Na przykład:

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;
  };
};
/ Align = "left" / / myAsyncjs ' możesz zrobić coś podobnego do poniższego w sąsiednim pliku:
...
/* 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
Author: Jay Edwards,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-26 09:07:26

Używając zadania, futurize i listy, możesz po prostu zrobić

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

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

Oto Jak to ustawisz

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)

Innym sposobem uporządkowania pożądanego kodu jest

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

A może nawet bardziej funkcjonalnie zorientowane

// 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)

Następnie z funkcji rodzica

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

Jeśli naprawdę chcesz większej elastyczności w kodowaniu, możesz to zrobić (dla Zabawy, używam proponowanego operatora Pipe Forward)

import { curry, flip } from 'ramda'

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

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

PS - ja nie spróbuj tego kodu na konsoli, może mieć jakieś literówki... "prosty freestyle, poza kopułą!"jak powiedzieliby dzieciaki z Lat 90. :- p

 1
Author: Babak,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-03 22:51:49

Oprócz @ Bergi ' s answer, chciałbym zaproponować trzecią alternatywę. Jest to bardzo podobne do drugiego przykładu @ Bergi, ale zamiast czekać na każdą readFile z osobna, tworzysz tablicę obietnic, z których każda czeka na końcu.

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

Zauważ, że funkcja przekazywana do .map() nie musi być async, ponieważ fs.readFile i tak zwraca obiekt Promise. Dlatego promises jest tablicą obiektów obietnicy, którą można wysłać do Promise.all().

W odpowiedzi @Bergi, konsola może rejestrować zawartość pliku nie w porządku. Na przykład, jeśli naprawdę mały plik zakończy odczyt przed naprawdę dużym plikiem, zostanie on najpierw zalogowany, nawet jeśli mały plik pojawi się po dużym pliku w tablicy files. Jednak w mojej metodzie powyżej masz gwarancję, że konsola będzie logować pliki w tej samej kolejności, w jakiej są odczytywane.

 1
Author: chharvey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-21 21:21:23

Jedno ważne zastrzeżenie jest następujące: metoda await + for .. of i Metoda forEach + async faktycznie mają różne efekty.

Posiadanie await wewnątrz prawdziwej pętli for upewni się, że wszystkie wywołania asynchroniczne są wykonywane jeden po drugim. I sposób forEach + async odpali wszystkie obietnice w tym samym czasie, co jest szybsze, ale czasami przytłoczone (jeśli wykonasz jakieś zapytanie DB lub odwiedzisz niektóre Usługi internetowe z ograniczeniami wolumenu i nie chcesz odpalić 100 000 połączeń na raz).

Możesz również użyć reduce + promise (mniej elegancki), jeśli nie używasz async/await i chcesz mieć pewność, że pliki są odczytywane jeden po drugim.

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

Lub możesz utworzyć forEachAsync, aby pomóc, ale zasadniczo użyć tego samego dla loop based.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
 0
Author: Leon li,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-24 20:00:43

Podobne do Antonio Val ' s p-iteration, alternatywnym modułem npm jest 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();

Alternatywnie, async-af posiada statyczną metodę (log/logAF), która rejestruje wyniki obietnic:

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

Jednak główną zaletą biblioteki jest to, że można łączyć asynchroniczne metody, aby zrobić coś takiego:

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
Author: Scott Rudiger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-21 16:55:47

Użyłbym dobrze przetestowanych (miliony pobrań tygodniowo) pify i async modułów. Jeśli nie jesteś zaznajomiony z modułem asynchronicznym, Gorąco polecam zapoznanie się z jego dokumentami . Widziałem wielu programistów marnujących czas na odtwarzanie swoich metod lub, co gorsza, na tworzenie trudnego do utrzymania kodu asynchronicznego, gdy metody asynchroniczne wyższego rzędu upraszczałyby kod.

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
```
 -2
Author: Zachary Ryan Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-04 16:03:47