Używanie asynchronizacji / oczekiwania z pętlą forEach

1128

Czy są jakieś problemy z używaniem async/ awaitw forEachpętli? Próbuję przeglądać tablicę plików i awaitzawartość 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? Kazałem mi powiedzieć, że nie powinieneś używać async/ awaitw takiej funkcji wyższego rzędu, więc chciałem tylko zapytać, czy jest z tym jakiś problem.

saadq
źródło

Odpowiedzi:

2144

Pewnie, że kod działa, ale jestem prawie pewien, że nie działa tak, jak tego oczekujesz. Po prostu odpala wiele asynchronicznych wywołań, ale printFilesfunkcja natychmiast po tym powraca.

Czytanie w kolejności

Jeśli chcesz czytać pliki po kolei, nie możesz tego użyćforEach . for … ofZamiast tego użyj nowoczesnej pętli, w której awaitbę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);
  }
}

Czytanie równoległe

Jeśli chcesz czytać pliki równolegle, nie możesz tego użyćforEach . Każde asyncwywołanie funkcji zwrotnej zwraca obietnicę, ale wyrzucasz je zamiast czekać na nie. mapZamiast tego użyj , a możesz poczekać na szereg obietnic, które otrzymasz 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)
  }));
}
Bergi
źródło
33
Czy możesz wyjaśnić, dlaczego for ... of ...działa?
Demonbane
84
ok wiem, dlaczego ... Używanie Babel przekształci async/ awaitgeneruje funkcję generatora, a użycie forEachoznacza, że ​​każda iteracja ma osobną funkcję generatora, która nie ma nic wspólnego z innymi. więc będą wykonywane niezależnie i nie mają kontekstu next()z innymi. W rzeczywistości prosta for()pętla działa również, ponieważ iteracje są również w funkcji pojedynczego generatora.
Demonbane,
21
@Demonbane: Krótko mówiąc, ponieważ został zaprojektowany do pracy :-) awaitzawiesza bieżącą ocenę funkcji , w tym wszystkie struktury kontrolne. Tak, pod tym względem jest dość podobny do generatorów (dlatego są one używane do asynchronizacji / oczekiwania na polifill).
Bergi,
3
@ arve0 Nie bardzo, asyncfunkcja jest całkiem inna niż Promisewywołanie zwrotne executora, ale tak, mapcallback zwraca obietnicę w obu przypadkach.
Bergi,
5
Kiedy przychodzisz, aby dowiedzieć się o obietnicach JS, zamiast tego użyj pół godziny na tłumaczenie łaciny. Mam nadzieję, że jesteś dumny @Bergi;)
Félix Gagnon-Grenier
188

Dzięki ES2018 możesz 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: iteracja-propozycja-asynchronizacja


2018-09-10: Ta odpowiedź cieszy się ostatnio dużym zainteresowaniem, więcej informacji na temat iteracji asynchronicznej można znaleźć na blogu Axela Rauschmayera: ES2018: iteracja asynchroniczna

Francisco Mateo
źródło
4
Pozytywne, byłoby wspaniale, gdybyś umieścił link do specyfikacji w swojej odpowiedzi dla każdego, kto chce wiedzieć więcej o iteracji asynchronicznej.
saadq
8
Nie powinna to być zawartość zamiast pliku w iteratorze
FluffyBeing
10
Dlaczego ludzie popierają tę odpowiedź? Przyjrzyj się bliżej odpowiedzi, pytaniu i propozycji. Po ofpowinna być funkcja asynchroniczna, która zwróci tablicę. To nie działa i Francisco powiedział;
Jewhenii Herasymchuk
3
Całkowicie zgadzam się z @AntonioVal. To nie jest odpowiedź.
Jewhenii Herasymchuk
2
Chociaż zgadzam się, że to nie jest odpowiedź, poprawienie propozycji jest sposobem na zwiększenie jej popularności, potencjalnie udostępniając ją wcześniej do późniejszego wykorzystania.
Robert Molina
61

Zamiast Promise.allw połączeniu z Array.prototype.map(co nie gwarantuje kolejności, w której Promises są rozwiązywane), używam Array.prototype.reduce, zaczynając od rozwiązanej 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());
}
Timothy Zorn
źródło
1
Działa to doskonale, dziękuję bardzo. Czy możesz wyjaśnić, co się tutaj dzieje Promise.resolve()i await promise;?
parrker9
1
To jest całkiem niezłe. Czy mam rację, sądząc, że pliki będą odczytywane w kolejności, a nie wszystkie naraz?
GollyJer
1
@ parrker9 Promise.resolve()zwraca już rozwiązany Promiseobiekt, więc reducemusi on Promisezaczynać. await promise;będzie czekać na Promiserozstrzygnięcie ostatniego w łańcuchu. @GollyJer Pliki będą przetwarzane sekwencyjnie, po jednym na raz.
Timothy Zorn,
Bardzo fajne użycie redukcji, dzięki za komentarz! Po prostu zaznaczę, że w przeciwieństwie do niektórych innych metod wymienionych w komentarzach, ta jest synchroniczna, co oznacza, że ​​pliki są odczytywane jeden po drugim, a nie równolegle (ponieważ następna iteracja funkcji zmniejszania zależy od poprzedniej iteracja, musi być synchroniczna).
Shay Yzhakov
1
@Shay, masz na myśli sekwencyjny, a nie synchroniczny. Jest to nadal asynchroniczne - jeśli zaplanowane są inne rzeczy, będą one działać między iteracjami tutaj.
Timothy Zorn
32

Moduł p-iteracyjny na npm implementuje metody iteracji Array, dzięki czemu można ich używać w bardzo prosty sposób za pomocą asynchronizacji / oczekiwania.

Przykład z twoją sprawą:

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);
  });
})();
Antonio Val
źródło
1
Podoba mi się, ponieważ ma te same funkcje / metody, co sam JS - w moim przypadku someraczej tego potrzebowałem forEach. Dzięki!
mikemaccana
25

Oto kilka forEachAsyncprototypów. Pamiętaj, że musisz awaitim:

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

Pamiętaj , że chociaż możesz to uwzględnić we własnym kodzie, nie powinieneś umieszczać tego w bibliotekach, które dystrybuujesz innym (aby uniknąć zanieczyszczenia ich globali).

Matt
źródło
1
Chociaż waham się dodać rzeczy bezpośrednio do prototypu, jest to niezła asynchronizacja dla każdej implementacji
DaniOcean
2
Tak długo, jak nazwa będzie unikalna w przyszłości (jak bym użył _forEachAsync), jest to uzasadnione. Myślę też, że jest to najładniejsza odpowiedź, ponieważ pozwala zaoszczędzić sporo kodu z podstawowymi informacjami.
mikemaccana
1
@estus To jest, aby uniknąć zanieczyszczenia kodu innych osób. Jeśli kod należy do naszej osobistej organizacji, a globale znajdują się w dobrze zidentyfikowanym pliku ( globals.jsbyłoby dobrze), możemy dodawać globale według własnego uznania.
mikemaccana
1
@mikemaccana To w celu uniknięcia ogólnie przyjętych złych praktyk. To prawda, można to zrobić tak długo, jak używasz tylko własnego kodu, co zdarza się rzadko. Problem polega na tym, że kiedy używasz bibliotek stron trzecich, może być jakiś inny facet, który czuje to samo i modyfikuje te same globale, tylko dlatego, że wydawało się to dobrym pomysłem w czasie, gdy biblioteka była pisana.
Estus Flask
1
@estus Sure. Dodałem ostrzeżenie do pytania, aby zapisać tutaj (niezbyt produktywną) dyskusję tutaj.
mikemaccana
6

Oprócz odpowiedzi @ Bergi chciałbym zaoferować trzecią alternatywę. Jest bardzo podobny do drugiego przykładu @ Bergi, ale zamiast readFileczekać na każdego z osobna, tworzysz szereg obietnic, na które czekasz 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 przekazana funkcja .map()nie musi być async, ponieważ fs.readFilei tak zwraca obiekt Promise. Dlatego promisesjest tablica obiektów Promise, do których można wysłać Promise.all().

W odpowiedzi @ Bergi konsola może rejestrować zawartość pliku w kolejności, w jakiej są czytane. Na przykład, jeśli naprawdę mały plik zakończy czytanie przed naprawdę dużym plikiem, zostanie najpierw zalogowany, nawet jeśli mały plik pojawia się za dużym plikiem w filestablicy. Jednak w powyższej metodzie masz gwarancję, że konsola będzie rejestrować pliki w tej samej kolejności, co podana tablica.

chharvey
źródło
1
Jestem prawie pewien, że się mylisz: jestem pewien, że twoja metoda może również odczytywać pliki w porządku. Tak, rejestruje dane wyjściowe we właściwej kolejności (z powodu await Promise.all), ale pliki mogły zostać odczytane w innej kolejności, co jest sprzeczne z Twoim stwierdzeniem „masz gwarancję, że konsola będzie logować pliki w tej samej kolejności, w jakiej są czytać".
Venryx
1
@Venryx Masz rację, dziękuję za korektę. Zaktualizowałem swoją odpowiedź.
chharvey
5

Rozwiązanie Bergi działa dobrze, gdy fsjest oparte na obietnicach. Można użyć bluebird, fs-extralub fs-promiseza to.

Jednak rozwiązanie dla natywnej fsbiblioteki węzłów jest następujące:

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

Uwaga: require('fs') obowiązkowo przyjmuje funkcję jako trzeci argument, w przeciwnym razie generuje błąd:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
myDoggyWritesCode
źródło
4

Oba powyższe rozwiązania działają, jednak Antonio wykonuje pracę z mniejszym kodem, oto jak pomógł mi rozwiązać dane z mojej bazy danych, z kilku różnych referencji potomnych, a następnie wepchnął je wszystkie do tablicy i rozwiązał to w obietnicy gotowy:

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)))
Hooman Askari
źródło
3

dość łatwo jest wstawić kilka metod w pliku, który będzie obsługiwał asynchroniczne dane w serializowanej kolejności i nada Twojemu kodowi bardziej konwencjonalny smak. 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;
  };
};

teraz, zakładając, że jest zapisany w „./myAsync.js”, 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));
}
Jay Edwards
źródło
2
Drobny aneks, nie zapomnij owinąć oczekujących / asynchronicznych bloków try / catch !!
Jay Edwards,
3

Jak odpowiedź @ Bergi, ale z jedną różnicą.

Promise.all odrzuca wszystkie obietnice, jeśli ktoś zostanie odrzucony.

Użyj rekurencji.

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

readFilesQueuewystępuje poza printFilesprzyczyną działania niepożądanego * wprowadzonego przezconsole.log , lepiej kpić, testować i szpiegować, więc nie jest fajnie mieć funkcję, która zwraca treść (sidenote).

Dlatego kod można po prostu zaprojektować w ten sposób: trzy oddzielne funkcje, które są „czyste” ** i nie wprowadzają żadnych skutków ubocznych, przetwarzają całą listę i mogą być łatwo modyfikowane w celu obsługi nieudanych przypadków.

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)

Przyszła edycja / aktualny stan

Węzeł obsługuje najwyższy poziom oczekiwania (nie ma jeszcze wtyczki, nie będzie i może być włączony za pomocą flag harmonii), jest fajny, ale nie rozwiązuje jednego problemu (strategicznie pracuję tylko na wersjach LTS). Jak zdobyć pliki?

Używanie kompozycji. Biorąc pod uwagę kod, sprawia mi wrażenie, że jest w module, więc powinien mieć jakąś funkcję, aby to zrobić. Jeśli nie, powinieneś użyć IIFE do zawinięcia kodu roli w funkcję asynchroniczną, tworząc prosty moduł, który zrobi wszystko za Ciebie, lub możesz iść właściwą drogą, jest tam kompozycja.

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

Zauważ, że nazwa zmiennej zmienia się z powodu semantyki. Zdajesz funktor (funkcję, którą można wywołać za pomocą innej funkcji) i otrzymuje wskaźnik pamięci, który zawiera początkowy blok logiki aplikacji.

Ale jeśli nie jest to moduł i musisz wyeksportować logikę?

Zawiń funkcje w funkcję asynchroniczną.

export const readFilesQueue = async () => {
    // ... to code goes here
}

Lub zmień nazwy zmiennych, cokolwiek ...


* przez efekt uboczny oznacza każdy efekt bakteryjny aplikacji, który może zmienić błędy statyczne / zachowanie lub introuce w aplikacji, takie jak IO.

** przez „czysty”, jest w apostrofie, ponieważ funkcje nie są czyste, a kod można konwertować na czystą wersję, gdy nie ma wyjścia konsoli, tylko manipulacje danymi.

Poza tym, aby być czystym, musisz pracować z monadami, które obsługują efekt uboczny, które są podatne na błędy i traktuje ten błąd osobno od aplikacji.

lukaswilkeer
źródło
2

Ważnym zastrzeżeniem jest to, że await + for .. ofmetoda i forEach + asyncsposób mają inny wpływ.

Posiadanie awaitwewnątrz prawdziwej forpętli zapewni, że wszystkie wywołania asynchroniczne będą wykonywane jeden po drugim. A forEach + asyncsposób wyzwoli wszystkie obietnice w tym samym czasie, co jest szybsze, ale czasem przytłoczone ( jeśli wykonasz zapytanie DB lub odwiedzisz niektóre serwisy internetowe z ograniczeniami głośności i nie chcesz uruchamiać 100 000 połączeń jednocześnie).

Możesz także użyć reduce + promise(mniej eleganckiego), jeśli nie używasz async/awaiti chcesz mieć pewność, że pliki będą odczytywane jeden po drugim .

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

Możesz też utworzyć forEachAsync, aby pomóc, ale w zasadzie użyj tego samego dla pętli bazowej.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
LeOn - Han Li
źródło
Zobacz, jak zdefiniować metodę w javascript na Array.prototype i Object.prototype, aby nie pojawiała się w pętli for . Prawdopodobnie powinieneś również użyć tej samej iteracji co natywna forEach- uzyskując dostęp do indeksów zamiast polegać na iteracji - i przekazać indeks do wywołania zwrotnego.
Bergi,
Możesz używać Array.prototype.reducew sposób wykorzystujący funkcję asynchroniczną. Pokazałem przykład w mojej odpowiedzi: stackoverflow.com/a/49499491/2537258
Timothy Zorn
2

Korzystając z Task, futurize i listy, którą możesz przejść, możesz po prostu to 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 skonfigurować

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 na uporządkowanie 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

// 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 nadrzędnej

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

Jeśli naprawdę chciałeś większej elastyczności kodowania, możesz to zrobić (dla zabawy korzystam z proponowanego operatora Pipe Forward )

import { curry, flip } from 'ramda'

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

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

PS - nie wypróbowałem tego kodu na konsoli, może mieć literówki ... „prosto freestyle, z góry kopuły!” jak powiedzieliby dzieci z lat 90. :-p

Babakness
źródło
2

Obecnie prototypowa właściwość Array.forEach nie obsługuje operacji asynchronicznych, ale możemy stworzyć własne wypełnienie spełniające nasze potrzeby.

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

I to wszystko! Masz teraz asynchroniczną metodę forEach dostępną dla dowolnych tablic, które zostały zdefiniowane po nich dla operacji.

Przetestujmy to ...

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

Możemy zrobić to samo dla niektórych innych funkcji tablicy, takich jak 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

... i tak dalej :)

Kilka rzeczy do zapamiętania:

  • Funkcja iteratora musi być funkcją asynchroniczną lub obietnicą
  • Wszystkie utworzone wcześniej tablice Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>nie będą miały tej funkcji
Amant
źródło
2

Właśnie dodając do oryginalnej odpowiedzi

  • Równoległa składnia odczytu w oryginalnej odpowiedzi jest czasami myląca i trudna do odczytania, być może możemy napisać ją w inny sposób
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);
}
  • W przypadku operacji sekwencyjnej działa nie tylko ... normalna pętla
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);
  }
}
gsaandy
źródło
1

Dzisiaj natknąłem się na wiele rozwiązań w tym zakresie. Uruchomienie asynchronizacji oczekuje na funkcje w pętli forEach. Budując opakowanie, możemy to zrobić.

Bardziej szczegółowe wyjaśnienie, jak to działa wewnętrznie, dla natywnego forEach i dlaczego nie jest w stanie wykonać wywołania funkcji asynchronicznej, a inne szczegóły na temat różnych metod znajdują się w linku tutaj

Wiele sposobów, dzięki którym można to zrobić, a są one następujące,

Metoda 1: Korzystanie z opakowania.

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

Metoda 2: Używanie tego samego jako funkcji ogólnej w 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');
            }
        })
      });
    };
  }

Stosowanie :

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

Metoda 3:

Korzystanie z Promise.all

  await Promise.all(items.map(async (item) => {
        await someAPICall();
        count++;
    }));

    console.log("count = " + count);

Metoda 4: Tradycyjna dla pętli lub nowoczesna dla pętli

// 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);
PranavKAndro
źródło
Twoje metody 1 i 2 są po prostu niepoprawnymi implementacjami, w których Promise.allpowinny zostać użyte - nie uwzględniają żadnego z wielu przypadków skrajnych.
Bergi
@Bergi: Dziękujemy za prawidłowe komentarze. Czy mógłbyś mi wyjaśnić, dlaczego metoda 1 i 2 są nieprawidłowe? Służy to również celowi. To działa bardzo dobrze. To znaczy, że wszystkie te metody są możliwe, w zależności od sytuacji, w której można zdecydować się na wybór. Mam działający przykład na to samo.
PranavKAndro
Nie działa na pustych tablicach, nie ma obsługi błędów i prawdopodobnie więcej problemów. Nie wymyślaj koła ponownie. Po prostu użyj Promise.all.
Bergi,
W pewnych warunkach, w których nie będzie to możliwe, będzie to pomocne. Obsługa błędów jest domyślnie wykonywana przez forEach api, więc nie ma problemów. Zadbano o to!
PranavKAndro
Nie, nie ma warunków, w których Promise.allnie jest to możliwe, ale async/ awaitjest. I nie, forEachabsolutnie nie obsługuje żadnych obietnic błędów.
Bergi,
1

To rozwiązanie jest również zoptymalizowane pod kątem pamięci, dzięki czemu można go uruchomić na 10 000 elementów danych i żądań. Niektóre inne rozwiązania tutaj spowodują awarię serwera na dużych zestawach danych.

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

Jak używać?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
Oliver Dixon
źródło
1

Możesz użyć Array.prototype.forEach, ale async / await nie jest tak kompatybilny. Jest tak, ponieważ obietnica zwrócona z wywołania zwrotnego asynchronicznego oczekuje, że zostanie rozwiązana, aleArray.prototype.forEach nie rozwiązuje żadnych obietnic z wykonania jej wywołania zwrotnego. Więc możesz użyć forEach, ale będziesz musiał sam poradzić sobie z obietnicą.

Oto sposób na odczyt i wydruk każdego pliku w szeregu przy użyciu 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
}

Oto sposób (wciąż używany Array.prototype.forEach) do równoległego drukowania zawartości plików

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)
}
richytong
źródło
Pierwszy senario jest idealny dla pętli, które muszą być uruchamiane w
seriach,
0

Podobnie jak Antonio Val's p-iteration, alternatywny moduł npm to 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-afma metodę statyczną (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 połączyć metody asynchroniczne, aby wykonać 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

Scott Rudiger
źródło
0

Aby zobaczyć, jak to może pójść nie tak, wydrukuj console.log na końcu metody.

Rzeczy, które ogólnie mogą pójść nie tak:

  • Arbitralny porządek.
  • printFiles może zakończyć działanie przed wydrukowaniem plików.
  • Kiepska wydajność.

Nie zawsze są one błędne, ale często występują w standardowych przypadkach użycia.

Ogólnie rzecz biorąc, użycie forEach da wszystko oprócz ostatniego. Wywoła każdą funkcję bez oczekiwania na funkcję, co oznacza, że ​​wszystkie funkcje mają się uruchomić, a następnie zakończyć, nie czekając na zakończenie funkcji.

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

Jest to przykład w natywnym JS, który zachowa porządek, zapobiegnie przedwczesnemu powrotowi funkcji i teoretycznie zachowa optymalną wydajność.

Spowoduje to:

  • Zainicjuj wszystkie odczyty pliku, aby miały miejsce równolegle.
  • Zachowaj porządek za pomocą map do mapowania nazw plików na obietnice oczekiwania.
  • Poczekaj na każdą obietnicę w kolejności określonej przez tablicę.

Dzięki temu rozwiązaniu pierwszy plik zostanie wyświetlony, gdy tylko będzie dostępny, bez konieczności czekania, aż pozostałe będą dostępne jako pierwsze.

Będzie również ładować wszystkie pliki jednocześnie, zamiast czekać na zakończenie pierwszego, zanim rozpocznie się odczyt drugiego pliku.

Jedyną wadą tej i oryginalnej wersji jest to, że jeśli wielokrotne odczyty są uruchamiane jednocześnie, trudniej jest obsługiwać błędy z powodu większej liczby błędów, które mogą wystąpić jednocześnie.

W wersjach, które odczytują plik na raz, zatrzyma się on na awarii bez marnowania czasu na próby odczytania kolejnych plików. Nawet przy skomplikowanym systemie anulowania może być trudne uniknięcie awarii pierwszego pliku, ale także odczytu większości innych plików.

Wydajność nie zawsze jest przewidywalna. Podczas gdy wiele systemów będzie działało szybciej z równoległymi odczytami plików, niektóre wolą sekwencję. Niektóre są dynamiczne i mogą się przesuwać pod obciążeniem, optymalizacje, które oferują opóźnienia, nie zawsze dają dobrą przepustowość przy silnej rywalizacji.

W tym przykładzie nie ma również obsługi błędów. Jeśli coś wymaga, aby albo wszystkie zostały pomyślnie pokazane, albo wcale, nie zrobi tego.

Dogłębnie zaleca się eksperymentowanie z plikiem console.log na każdym etapie i fałszywymi rozwiązaniami do odczytu plików (zamiast tego losowe opóźnienie). Chociaż wiele rozwiązań wydaje się robić to samo w prostych przypadkach, wszystkie mają subtelne różnice, które wymagają dodatkowej analizy, aby je wycisnąć.

Użyj tej makiety, aby odróżnić rozwiązania:

(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}`);
})();
jgmjgm
źródło
-3

Korzystałbym ze sprawdzonych (miliony pobrań tygodniowo) modułów pify i asynchronicznych . Jeśli nie znasz modułu asynchronicznego, zdecydowanie zalecamy sprawdzenie jego dokumentacji . Widziałem, jak wielu deweloperów marnuje czas na odtwarzanie swoich metod lub, co gorsza, utrudnia utrzymanie kodu asynchronicznego, gdy metody asynchroniczne wyższego rzędu uprościł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
```

Zachary Ryan Smith
źródło
To krok w złym kierunku. Oto przewodnik po mapach, który stworzyłem, aby pomóc ludziom utknąć w piekle wywołania zwrotnego w erze współczesnej JS: github.com/jmjpro/async-package-to-async-await/blob/master/… .
jbustamovej
jak widać tutaj , jestem zainteresowany i otwarty na async / await zamiast biblioteki async. W tej chwili myślę, że każdy ma swój czas i miejsce. Nie jestem przekonany, że async lib == „callback hell” i async / await == „era współczesnego JS”. imo, gdy async lib> async / czekają: 1. złożony przepływ (np. kolejka, ładunek, a nawet auto, gdy sprawy się komplikują) 2. współbieżność 3. obsługa tablic / obiektów / iteratorów 4. obsługa błędów
Zachary Ryan Smith