Modo corretto per scrivere loop per promessa.


116

Come costruire correttamente un ciclo per assicurarsi che la seguente chiamata alla promessa e il logger.log (res) concatenato vengano eseguiti in modo sincrono attraverso l'iterazione? (bluebird)

db.getUser(email).then(function(res) { logger.log(res); }); // this is a promise

Ho provato nel modo seguente (metodo da http://blog.victorquinn.com/javascript-promise-while-loop )

var Promise = require('bluebird');

var promiseWhile = function(condition, action) {
    var resolver = Promise.defer();

    var loop = function() {
        if (!condition()) return resolver.resolve();
        return Promise.cast(action())
            .then(loop)
            .catch(resolver.reject);
    };

    process.nextTick(loop);

    return resolver.promise;
});

var count = 0;
promiseWhile(function() {
    return count < 10;
}, function() {
    return new Promise(function(resolve, reject) {
        db.getUser(email)
          .then(function(res) { 
              logger.log(res); 
              count++;
              resolve();
          });
    }); 
}).then(function() {
    console.log('all done');
}); 

Anche se sembra funzionare, ma non credo che garantisca l'ordine di chiamata a logger.log (res);

Eventuali suggerimenti?


1
Il codice mi sembra a posto (la ricorsione con la loopfunzione è il modo per eseguire cicli sincroni). Perché pensi che non ci siano garanzie?
hugomg

db.getUser (email) è garantito per essere chiamato in ordine. Tuttavia, poiché db.getUser () è di per sé una promessa, chiamarlo sequenzialmente non significa necessariamente che le query del database per "email" vengano eseguite in sequenza a causa della caratteristica asincrona di promise. Pertanto, il logger.log (res) viene richiamato a seconda di quale query finisce per prima.
user2127480

1
@ user2127480: Ma la successiva iterazione del ciclo viene chiamata sequenzialmente solo dopo che la promessa è stata risolta, è così che whilefunziona il codice?
Bergi

Risposte:


78

Non credo che garantisca l'ordine di chiamata a logger.log (res);

In realtà, lo fa. Questa istruzione viene eseguita prima della resolvechiamata.

Eventuali suggerimenti?

Molte. La cosa più importante è che utilizzi l' antipattern crea-promessa-manualmente - fai solo

promiseWhile(…, function() {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 count++;
             });
})…

In secondo luogo, quella whilefunzione potrebbe essere molto semplificata:

var promiseWhile = Promise.method(function(condition, action) {
    if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Terzo, non userei un whileciclo (con una variabile di chiusura) ma un forciclo:

var promiseFor = Promise.method(function(condition, action, value) {
    if (!condition(value)) return value;
    return action(value).then(promiseFor.bind(null, condition, action));
});

promiseFor(function(count) {
    return count < 10;
}, function(count) {
    return db.getUser(email)
             .then(function(res) { 
                 logger.log(res); 
                 return ++count;
             });
}, 0).then(console.log.bind(console, 'all done'));

2
Ops. Tranne che actionprende valuecome argomento in promiseFor. Quindi non mi permetterebbe di fare una modifica così piccola. Grazie, è molto utile ed elegante.
Gordon

1
@ Roamer-1888: Forse la terminologia è un po 'strana, ma intendo dire che un whileciclo verifica uno stato globale mentre un forciclo ha la sua variabile di iterazione (contatore) associata al corpo del ciclo stesso. In effetti, ho utilizzato un approccio più funzionale che assomiglia più a un'iterazione di punto fisso che a un ciclo. Controlla di nuovo il loro codice, il valueparametro è diverso.
Bergi

2
OK, ora lo vedo. Poiché .bind()offusca il nuovo value, penso che potrei scegliere di rendere la funzione a lungo termine per la leggibilità. E scusa se mi sto comportando bene, ma se promiseFore promiseWhilenon coesistono, allora come si chiama l'altro?
Roamer-1888

2
@herve Puoi sostanzialmente ometterlo e sostituire il return …con return Promise.resolve(…). Se hai bisogno di ulteriori salvaguardie contro conditiono actionlancia un'eccezione (come la Promise.methodfornisce ), avvolgi l'intero corpo della funzione in unreturn Promise.resolve().then(() => { … })
Bergi

2
@herve In realtà dovrebbe essere Promise.resolve().then(action).…o Promise.resolve(action()).…, non è necessario racchiudere il valore di ritorno dithen
Bergi

134

Se vuoi davvero una promiseWhen()funzione generale per questo e altri scopi, allora fallo assolutamente, usando le semplificazioni di Bergi. Tuttavia, a causa del modo in cui promette di funzionare, passare i callback in questo modo non è generalmente necessario e ti costringe a saltare attraverso complessi piccoli cerchi.

Per quanto posso dire che stai provando:

  • per recuperare in modo asincrono una serie di dettagli utente per una raccolta di indirizzi e-mail (almeno, questo è l'unico scenario che ha senso).
  • per farlo costruendo una .then()catena tramite la ricorsione.
  • per mantenere l'ordine originale durante la gestione dei risultati restituiti.

Definito così, il problema è in realtà quello discusso in "The Collection Kerfuffle" in Promise Anti-patterns , che offre due semplici soluzioni:

  • chiamate asincrone parallele utilizzando Array.prototype.map()
  • chiamate asincrone seriali utilizzando Array.prototype.reduce().

L'approccio parallelo darà (direttamente) il problema che stai cercando di evitare - che l'ordine delle risposte è incerto. L'approccio seriale costruirà la .then()catena richiesta - piatta - nessuna ricorsione.

function fetchUserDetails(arr) {
    return arr.reduce(function(promise, email) {
        return promise.then(function() {
            return db.getUser(email).done(function(res) {
                logger.log(res);
            });
        });
    }, Promise.resolve());
}

Chiama come segue:

//Compose here, by whatever means, an array of email addresses.
var arrayOfEmailAddys = [...];

fetchUserDetails(arrayOfEmailAddys).then(function() {
    console.log('all done');
});

Come puoi vedere, non c'è bisogno della brutta var esterna counto della sua conditionfunzione associata . Il limite (di 10 nella domanda) è determinato interamente dalla lunghezza dell'array arrayOfEmailAddys.


16
sembra che questa dovrebbe essere la risposta selezionata. approccio grazioso e molto riutilizzabile.
ken

1
Qualcuno sa se una cattura si propagherebbe al genitore? Ad esempio, se db.getUser dovesse fallire, l'errore (rifiuto) si propagherebbe di nuovo?
delfuturo

@wayofthefuture, no. Pensala in questo modo ..... non puoi cambiare la storia.
Roamer-1888

4
Grazie per la risposta. Questa dovrebbe essere la risposta accettata.
klvs

1
@ Roamer-1888 Errore mio, ho letto male la domanda originale. Io (personalmente) stavo cercando una soluzione in cui l'elenco iniziale di cui hai bisogno per ridurre cresce man mano che le tue richieste si risolvono (è una queryPiù di un DB). In questo caso ho trovato l'idea di utilizzare reduce con un generatore una separazione abbastanza carina tra (1) l'estensione condizionale della catena di promesse e (2) il consumo dei risultati restituiti.
jhp

40

Ecco come lo faccio con l'oggetto Promise standard.

// Given async function sayHi
function sayHi() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('Hi');
      resolve();
    }, 3000);
  });
}

// And an array of async functions to loop through
const asyncArray = [sayHi, sayHi, sayHi];

// We create the start of a promise chain
let chain = Promise.resolve();

// And append each function in the array to the promise chain
for (const func of asyncArray) {
  chain = chain.then(func);
}

// Output:
// Hi
// Hi (After 3 seconds)
// Hi (After 3 more seconds)

Ottima risposta @youngwerth
Jam Risser

3
come inviare parametri in questo modo?
Akash khan

4
@khan sulla riga chain = chain.then (func), puoi fare: chain = chain.then(func.bind(null, "...your params here")); o chain = chain.then(() => func("your params here"));
youngwerth

9

Dato

  • funzione asyncFn
  • serie di articoli

necessario

  • prometti di concatenare .then () in serie (in ordine)
  • native es6

Soluzione

let asyncFn = (item) => {
  return new Promise((resolve, reject) => {
    setTimeout( () => {console.log(item); resolve(true)}, 1000 )
  })
}

// asyncFn('a')
// .then(()=>{return async('b')})
// .then(()=>{return async('c')})
// .then(()=>{return async('d')})

let a = ['a','b','c','d']

a.reduce((previous, current, index, array) => {
  return previous                                    // initiates the promise chain
  .then(()=>{return asyncFn(array[index])})      //adds .then() promise for each item
}, Promise.resolve())

2
Se asyncsta per diventare una parola riservata in JavaScript, potrebbe aggiungere chiarezza per rinominare quella funzione qui.
Hippietrail

Inoltre, non è il caso che la freccia grassa funzioni senza un corpo tra parentesi graffe restituisce semplicemente ciò che l'espressione lì valuta? Ciò renderebbe il codice più conciso. Potrei anche aggiungere un commento in cui currentsi afferma che non è utilizzato.
Hippietrail

2
questo è il modo corretto!
teleme.io


3

La funzione suggerita da Bergi è davvero carina:

var promiseWhile = Promise.method(function(condition, action) {
      if (!condition()) return;
    return action().then(promiseWhile.bind(null, condition, action));
});

Tuttavia, voglio fare una piccola aggiunta, che ha senso, quando si usano le promesse:

var promiseWhile = Promise.method(function(condition, action, lastValue) {
  if (!condition()) return lastValue;
  return action().then(promiseWhile.bind(null, condition, action));
});

In questo modo il ciclo while può essere incorporato in una catena di promesse e si risolve con lastValue (anche se action () non viene mai eseguita). Vedi esempio:

var count = 10;
util.promiseWhile(
  function condition() {
    return count > 0;
  },
  function action() {
    return new Promise(function(resolve, reject) {
      count = count - 1;
      resolve(count)
    })
  },
  count)

3

Farei qualcosa del genere:

var request = []
while(count<10){
   request.push(db.getUser(email).then(function(res) { return res; }));
   count++
};

Promise.all(request).then((dataAll)=>{
  for (var i = 0; i < dataAll.length; i++) {

      logger.log(dataAll[i]); 
  }  
});

in questo modo, dataAll è un array ordinato di tutti gli elementi da registrare. E l'operazione di registro verrà eseguita quando tutte le promesse saranno state fatte.


Promise.all chiamerà allo stesso tempo le promesse. Quindi l'ordine di completamento potrebbe cambiare. La domanda richiede promesse concatenate. Quindi l'ordine di completamento non dovrebbe essere modificato.
canbax

Modifica 1: non è necessario chiamare Promise.all. Finché le promesse saranno mantenute, verranno eseguite in parallelo.
canbax

1

Usa async e await (es6):

function taskAsync(paramets){
 return new Promise((reslove,reject)=>{
 //your logic after reslove(respoce) or reject(error)
})
}

async function fName(){
let arry=['list of items'];
  for(var i=0;i<arry.length;i++){
   let result=await(taskAsync('parameters'));
}

}

0
function promiseLoop(promiseFunc, paramsGetter, conditionChecker, eachFunc, delay) {
    function callNext() {
        return promiseFunc.apply(null, paramsGetter())
            .then(eachFunc)
    }

    function loop(promise, fn) {
        if (delay) {
            return new Promise(function(resolve) {
                setTimeout(function() {
                    resolve();
                }, delay);
            })
                .then(function() {
                    return promise
                        .then(fn)
                        .then(function(condition) {
                            if (!condition) {
                                return true;
                            }
                            return loop(callNext(), fn)
                        })
                });
        }
        return promise
            .then(fn)
            .then(function(condition) {
                if (!condition) {
                    return true;
                }
                return loop(callNext(), fn)
            })
    }

    return loop(callNext(), conditionChecker);
}


function makeRequest(param) {
    return new Promise(function(resolve, reject) {
        var req = https.request(function(res) {
            var data = '';
            res.on('data', function (chunk) {
                data += chunk;
            });
            res.on('end', function () {
                resolve(data);
            });
        });
        req.on('error', function(e) {
            reject(e);
        });
        req.write(param);
        req.end();
    })
}

function getSomething() {
    var param = 0;

    var limit = 10;

    var results = [];

    function paramGetter() {
        return [param];
    }
    function conditionChecker() {
        return param <= limit;
    }
    function callback(result) {
        results.push(result);
        param++;
    }

    return promiseLoop(makeRequest, paramGetter, conditionChecker, callback)
        .then(function() {
            return results;
        });
}

getSomething().then(function(res) {
    console.log('results', res);
}).catch(function(err) {
    console.log('some error along the way', err);
});

0

Che ne dici di questo usando BlueBird ?

function fetchUserDetails(arr) {
    return Promise.each(arr, function(email) {
        return db.getUser(email).done(function(res) {
            logger.log(res);
        });
    });
}

0

Ecco un altro metodo (ES6 w / std Promise). Utilizza criteri di uscita di tipo lodash / underscore (return === false). Notare che è possibile aggiungere facilmente un metodo exitIf () nelle opzioni da eseguire in doOne ().

const whilePromise = (fnReturningPromise,options = {}) => { 
    // loop until fnReturningPromise() === false
    // options.delay - setTimeout ms (set to 0 for 1 tick to make non-blocking)
    return new Promise((resolve,reject) => {
        const doOne = () => {
            fnReturningPromise()
            .then((...args) => {
                if (args.length && args[0] === false) {
                    resolve(...args);
                } else {
                    iterate();
                }
            })
        };
        const iterate = () => {
            if (options.delay !== undefined) {
                setTimeout(doOne,options.delay);
            } else {
                doOne();
            }
        }
        Promise.resolve()
        .then(iterate)
        .catch(reject)
    })
};

0

Utilizzando l'oggetto promessa standard e facendo in modo che la promessa restituisca i risultati.

function promiseMap (data, f) {
  const reducer = (promise, x) =>
    promise.then(acc => f(x).then(y => acc.push(y) && acc))
  return data.reduce(reducer, Promise.resolve([]))
}

var emails = []

function getUser(email) {
  return db.getUser(email)
}

promiseMap(emails, getUser).then(emails => {
  console.log(emails)
})

0

Per prima cosa prendi l'array di promesse (array di promesse) e dopo aver risolto questi array di promesse usando Promise.all(promisearray).

var arry=['raju','ram','abdul','kruthika'];

var promiseArry=[];
for(var i=0;i<arry.length;i++) {
  promiseArry.push(dbFechFun(arry[i]));
}

Promise.all(promiseArry)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
     console.log(error);
  });

function dbFetchFun(name) {
  // we need to return a  promise
  return db.find({name:name}); // any db operation we can write hear
}
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.