Node JS Promise.all i forEach

120

Mam strukturę podobną do tablicy, która uwidacznia metody asynchroniczne. Metoda async wywołuje zwracające struktury tablicowe, które z kolei ujawniają więcej metod asynchronicznych. Tworzę kolejny obiekt JSON do przechowywania wartości uzyskanych z tej struktury, dlatego muszę uważać na śledzenie odwołań w wywołaniach zwrotnych.

Zakodowałem rozwiązanie brutalnej siły, ale chciałbym nauczyć się bardziej idiomatycznego lub czystego rozwiązania.

  1. Wzór powinien być powtarzalny dla n poziomów zagnieżdżenia.
  2. Muszę użyć promise.all lub innej podobnej techniki, aby określić, kiedy należy rozwiązać procedurę obejmującą.
  3. Nie każdy element będzie koniecznie wymagał wywołania asynchronicznego. Tak więc w zagnieżdżonej obietnicy.all nie mogę po prostu przypisywać elementów do mojej tablicy JSON na podstawie indeksu. Niemniej jednak muszę użyć czegoś takiego jak obietnica.all w zagnieżdżonym forEach, aby upewnić się, że wszystkie przypisania właściwości zostały dokonane przed rozwiązaniem procedury zamykającej.
  4. Używam bluebird promise lib, ale nie jest to wymagane

Oto fragment kodu -

var jsonItems = [];

items.forEach(function(item){

  var jsonItem = {};
  jsonItem.name = item.name;
  item.getThings().then(function(things){
  // or Promise.all(allItemGetThingCalls, function(things){

    things.forEach(function(thing, index){

      jsonItems[index].thingName = thing.name;
      if(thing.type === 'file'){

        thing.getFile().then(function(file){ //or promise.all?

          jsonItems[index].filesize = file.getSize();
user3205931
źródło
To jest link do działającego źródła, które chcę ulepszyć. github.com/pebanfield/change-view-service/blob/master/src/…
user3205931
1
Widzę w próbce używasz Bluebird, Bluebird rzeczywiście sprawia, że życie jeszcze łatwiej z Promise.map(równoległe) i Promise.each(sekwencyjnego) W tym przypadku, również uwaga Promise.deferjest przestarzała - kod w moich pokazów Odpowiedź Jak unikać jej przez powrocie obietnic. Obietnice dotyczą zwracanych wartości.
Benjamin Gruenbaum

Odpowiedzi:

368

Jest to całkiem proste dzięki kilku prostym zasadom:

  • Za każdym razem, gdy tworzysz obietnicę w then, zwróć ją - żadna obietnica, której nie zwrócisz, nie będzie czekana na zewnątrz.
  • Kiedykolwiek tworzysz wiele obietnic, .allone - w ten sposób czekają na wszystkie obietnice i żaden błąd z nich nie jest uciszany.
  • Ilekroć gniazdujesz then, zazwyczaj możesz powrócić na środek - thenłańcuchy są zwykle na głębokości co najwyżej 1 poziomu.
  • Ilekroć wykonujesz IO, powinno to być z obietnicą - albo powinno być w obietnicy, albo powinno używać obietnicy, aby zasygnalizować jej zakończenie.

I kilka wskazówek:

  • Mapowanie jest lepsze .mapniż zfor/push - jeśli odwzorowujesz wartości za pomocą funkcji, mappozwala zwięźle wyrazić koncepcję stosowania działań jeden po drugim i agregowania wyników.
  • Współbieżność jest lepsza niż wykonywanie sekwencyjne, jeśli jest bezpłatne - lepiej jest wykonywać rzeczy jednocześnie i czekać na nie, Promise.allniż wykonywać czynności jedna po drugiej - każda z nich czeka przed następną.

Ok, więc zaczynajmy:

var items = [1, 2, 3, 4, 5];
var fn = function asyncMultiplyBy2(v){ // sample async action
    return new Promise(resolve => setTimeout(() => resolve(v * 2), 100));
};
// map over forEach since it returns

var actions = items.map(fn); // run the function over all items

// we now have a promises array and we want to wait for it

var results = Promise.all(actions); // pass array of promises

results.then(data => // or just .then(console.log)
    console.log(data) // [2, 4, 6, 8, 10]
);

// we can nest this of course, as I said, `then` chains:

var res2 = Promise.all([1, 2, 3, 4, 5].map(fn)).then(
    data => Promise.all(data.map(fn))
).then(function(data){
    // the next `then` is executed after the promise has returned from the previous
    // `then` fulfilled, in this case it's an aggregate promise because of 
    // the `.all` 
    return Promise.all(data.map(fn));
}).then(function(data){
    // just for good measure
    return Promise.all(data.map(fn));
});

// now to get the results:

res2.then(function(data){
    console.log(data); // [16, 32, 48, 64, 80]
});
Benjamin Gruenbaum
źródło
5
Ach, pewne zasady z twojej perspektywy :-)
Bergi
1
@Bergi ktoś powinien naprawdę sporządzić listę tych zasad i krótkie tło dotyczące obietnic. Prawdopodobnie możemy go hostować na bluebirdjs.com.
Benjamin Gruenbaum,
skoro nie mam po prostu podziękować - ten przykład wygląda dobrze i podoba mi się sugestia mapy, jednak co zrobić z kolekcją obiektów, w których tylko niektóre mają metody asynchroniczne? (Mój punkt 3 powyżej) Pomyślałem, że abstrahowałbym logikę parsowania dla każdego elementu do funkcji, a następnie rozwiązałbym ją na odpowiedzi wywołania asynchronicznego lub tam, gdzie nie było wywołania asynchronicznego, po prostu rozwiązuj. Czy to ma sens?
user3205931
Muszę również mieć funkcję map, która zwraca zarówno obiekt json, który buduję, jak i wynik wywołania asynchronicznego, który muszę wykonać, więc nie jestem pewien, jak to zrobić - w końcu całość musi być rekurencyjna, ponieważ chodzę po katalogu struktura - nadal to
żuję,
2
Obietnice @ user3205931 są raczej proste niż łatwe , to znaczy - nie są tak znane jak inne rzeczy, ale kiedy już je poznasz, są o wiele lepsze w użyciu. Trzymaj się mocno, dostaniesz :)
Benjamin Gruenbaum
42

Oto prosty przykład użycia funkcji redukuj. Działa seryjnie, utrzymuje kolejność reklam i nie wymaga Bluebird.

/**
 * 
 * @param items An array of items.
 * @param fn A function that accepts an item from the array and returns a promise.
 * @returns {Promise}
 */
function forEachPromise(items, fn) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item);
        });
    }, Promise.resolve());
}

I użyj tego w ten sposób:

var items = ['a', 'b', 'c'];

function logItem(item) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            resolve();
        })
    });
}

forEachPromise(items, logItem).then(() => {
    console.log('done');
});

Okazało się, że przydatne jest wysyłanie opcjonalnego kontekstu do pętli. Kontekst jest opcjonalny i wspólny dla wszystkich iteracji.

function forEachPromise(items, fn, context) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item, context);
        });
    }, Promise.resolve());
}

Twoja funkcja obietnicy wyglądałaby następująco:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}
Steven Spungin
źródło
Dzięki za to - Twoje rozwiązanie zadziałało dla mnie tam, gdzie inne (w tym różne biblioteki npm) nie. Czy opublikowałeś to na npm?
SamF
Dziękuję Ci. Funkcja zakłada, że ​​wszystkie obietnice zostały rozwiązane. Jak postępujemy z odrzuconymi obietnicami? Ponadto, jak radzimy sobie z pomyślnymi obietnicami z wartością?
oyalhi
@oyalhi Sugerowałbym użycie „kontekstu” i dodanie tablicy odrzuconych parametrów wejściowych odwzorowanych na błąd. Tak naprawdę jest to przypadek użycia, ponieważ niektórzy będą chcieli zignorować wszystkie pozostałe obietnice, a niektórzy nie. W przypadku zwracanej wartości można również zastosować podobne podejście.
Steven Spungin
1

Miałem tę samą sytuację. Rozwiązałem za pomocą dwóch Promise.All ().

Myślę, że było to naprawdę dobre rozwiązanie, więc opublikowałem je na npm: https://www.npmjs.com/package/promise-foreach

Myślę, że twój kod będzie mniej więcej taki

var promiseForeach = require('promise-foreach')
var jsonItems = [];
promiseForeach.each(jsonItems,
    [function (jsonItems){
        return new Promise(function(resolve, reject){
            if(jsonItems.type === 'file'){
                jsonItems.getFile().then(function(file){ //or promise.all?
                    resolve(file.getSize())
                })
            }
        })
    }],
    function (result, current) {
        return {
            type: current.type,
            size: jsonItems.result[0]
        }
    },
    function (err, newList) {
        if (err) {
            console.error(err)
            return;
        }
        console.log('new jsonItems : ', newList)
    })
saulsluz
źródło
0

Aby dodać do przedstawionego rozwiązania, w moim przypadku chciałem pobrać wiele danych z Firebase do listy produktów. Oto jak to zrobiłem:

useEffect(() => {
  const fn = p => firebase.firestore().doc(`products/${p.id}`).get();
  const actions = data.occasion.products.map(fn);
  const results = Promise.all(actions);
  results.then(data => {
    const newProducts = [];
    data.forEach(p => {
      newProducts.push({ id: p.id, ...p.data() });
    });
    setProducts(newProducts);
  });
}, [data]);
Charles de Dreuille
źródło