Nodo JS Promise.all and forEach


120

Ho una struttura simile a un array che espone metodi asincroni. Il metodo asincrono chiama strutture di matrice di ritorno che a loro volta espongono più metodi asincroni. Sto creando un altro oggetto JSON per memorizzare i valori ottenuti da questa struttura e quindi devo stare attento a tenere traccia dei riferimenti nei callback.

Ho codificato una soluzione per la forza bruta, ma vorrei imparare una soluzione più idiomatica o pulita.

  1. Il modello dovrebbe essere ripetibile per n livelli di nidificazione.
  2. Ho bisogno di usare promise.all o una tecnica simile per determinare quando risolvere la routine di chiusura.
  3. Non tutti gli elementi implicheranno necessariamente l'esecuzione di una chiamata asincrona. Quindi, in una promessa annidata, non posso semplicemente fare assegnazioni ai miei elementi dell'array JSON in base all'indice. Tuttavia, ho bisogno di usare qualcosa come promise.all in forEach annidato per assicurarmi che tutte le assegnazioni di proprietà siano state fatte prima di risolvere la routine di inclusione.
  4. Sto usando bluebird promise lib ma questo non è un requisito

Ecco un po 'di codice parziale -

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

Questo è il collegamento alla fonte di lavoro che voglio migliorare. github.com/pebanfield/change-view-service/blob/master/src/…
user3205931

1
Vedo nell'esempio che stai usando bluebird, bluebird in realtà ti rende la vita ancora più semplice con Promise.map(simultaneo) e Promise.each(sequenziale) in questo caso, anche la nota Promise.deferè deprecata: il codice nella mia risposta mostra come evitarlo restituendo promesse. Le promesse riguardano tutti i valori di ritorno.
Benjamin Gruenbaum

Risposte:


368

È abbastanza semplice con alcune semplici regole:

  • Ogni volta che crei una promessa in a then, restituiscila : qualsiasi promessa che non restituisci non verrà aspettata all'esterno.
  • Ogni volta che crei più promesse, .allin questo modo aspetta tutte le promesse e nessun errore da nessuna di esse viene messo a tacere.
  • Ogni volta che si annidano theni messaggi, di solito si può tornare a metà : le thencatene sono solitamente profonde al massimo 1 livello.
  • Ogni volta che esegui l'IO, dovrebbe essere con una promessa : o dovrebbe essere in una promessa o dovrebbe utilizzare una promessa per segnalare il suo completamento.

E alcuni suggerimenti:

  • La mappatura viene eseguita meglio con .mapche confor/push : se si mappano valori con una funzione, è mappossibile esprimere in modo conciso l'idea di applicare le azioni una per una e di aggregare i risultati.
  • La concorrenza è migliore dell'esecuzione sequenziale se è gratuita : è meglio eseguire le cose contemporaneamente e aspettarle Promise.allpiuttosto che eseguirle una dopo l'altra, ognuna in attesa prima dell'altra.

Ok, quindi iniziamo:

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

5
Ah, alcune regole dal tuo punto di vista :-)
Bergi

1
@Bergi qualcuno dovrebbe davvero fare un elenco di queste regole e un breve background sulle promesse. Probabilmente possiamo ospitarlo su bluebirdjs.com.
Benjamin Gruenbaum

poiché non dovrei solo dire grazie - questo esempio sembra buono e mi piace il suggerimento della mappa, tuttavia, cosa fare con una raccolta di oggetti in cui solo alcuni hanno metodi asincroni? (Il mio punto 3 sopra) Avevo l'idea di astrarre la logica di analisi per ogni elemento in una funzione e quindi di risolverla sulla risposta alla chiamata asincrona o dove non c'era nessuna chiamata asincrona, semplicemente risolverla. Ha senso?
user3205931

Devo anche che la funzione map restituisca sia l'oggetto json che sto costruendo e il risultato della chiamata asincrona che devo fare quindi non sono sicuro di come farlo - finalmente l'intera cosa deve essere ricorsiva poiché sto camminando in una directory struttura - Ci sto ancora masticando ma il lavoro retribuito si sta
intromettendo

2
@ user3205931 le promesse sono semplici, piuttosto che facili , cioè non sono familiari come altre cose, ma una volta che le fai, sono molto meglio da usare. Tieni duro,
capirai

42

Ecco un semplice esempio di utilizzo di reduce. Funziona in serie, mantiene l'ordine di inserzione e non richiede 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());
}

E usalo in questo modo:

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

Abbiamo trovato utile inviare in loop un contesto opzionale. Il contesto è opzionale e condiviso da tutte le iterazioni.

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

La tua funzione di promessa sarebbe simile a questa:

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

Grazie per questo - la tua soluzione ha funzionato per me dove altri (comprese varie librerie npm) non hanno. L'hai pubblicato su npm?
SamF

Grazie. La funzione presuppone che tutte le promesse siano state risolte. Come gestiamo le promesse rifiutate? Inoltre, come gestiamo le promesse di successo con un valore?
oyalhi

@oyalhi suggerirei di utilizzare il "contesto" e di aggiungere un array di parametri di input rifiutati mappati all'errore. Questo è davvero per caso d'uso, poiché alcuni vorranno ignorare tutte le promesse rimanenti e altri no. Per il valore restituito, puoi anche utilizzare un approccio simile.
Steven Spungin

1

Ho passato la stessa situazione. Ho risolto usando due Promise.All ().

Penso che fosse davvero una buona soluzione, quindi l'ho pubblicata su npm: https://www.npmjs.com/package/promise-foreach

Penso che il tuo codice sarà qualcosa del genere

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

0

Giusto per aggiungere alla soluzione presentata, nel mio caso volevo recuperare più dati da Firebase per un elenco di prodotti. Ecco come l'ho fatto:

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]);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.