Bonne façon d'écrire des boucles pour la promesse.


116

Comment construire correctement une boucle pour vous assurer que l' appel de promesse suivant et le logger.log enchaîné (res) s'exécutent de manière synchrone via l'itération? (oiseau bleu)

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

J'ai essayé la méthode suivante (méthode de 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');
}); 

Bien que cela semble fonctionner, mais je ne pense pas que cela garantit l'ordre d'appeler logger.log (res);

Aucune suggestion?


1
Le code me semble correct (la récursivité avec la loopfonction est le moyen de faire des boucles synchrones). Pourquoi pensez-vous qu'il n'y a aucune garantie?
hugomg

L'appel de db.getUser (email) est garanti dans l'ordre. Mais, puisque db.getUser () lui-même est une promesse, l'appeler séquentiellement ne signifie pas nécessairement que les requêtes de base de données pour «e-mail» s'exécutent séquentiellement en raison de la fonctionnalité asynchrone de la promesse. Ainsi, le logger.log (res) est appelé en fonction de la requête qui se termine en premier.
user2127480

1
@ user2127480: Mais la prochaine itération de la boucle est appelée séquentiellement seulement après la résolution de la promesse, c'est comme ça que ce whilecode fonctionne?
Bergi

Réponses:


78

Je ne pense pas que cela garantisse l'ordre d'appeler logger.log (res);

En fait, c'est le cas. Cette instruction est exécutée avant l' resolveappel.

Aucune suggestion?

Beaucoup. Le plus important est votre utilisation de l' anti-modèle create-promise-manual - faites simplement

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

Deuxièmement, cette whilefonction pourrait être beaucoup simplifiée:

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

Troisièmement, je n'utiliserais pas une whileboucle (avec une variable de fermeture) mais une forboucle:

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
Oups. Sauf que cela actionprend valuecomme argument dans promiseFor. Alors je ne me laisserais pas faire une si petite modification. Merci, c'est très utile et élégant.
Gordon

1
@ Roamer-1888: Peut-être que la terminologie est un peu étrange, mais je veux dire qu'une whileboucle teste un état global alors qu'une forboucle a sa variable d'itération (compteur) liée au corps de la boucle lui-même. En fait, j'ai utilisé une approche plus fonctionnelle qui ressemble plus à une itération de point fixe qu'à une boucle. Vérifiez à nouveau leur code, le valueparamètre est différent.
Bergi

2
OK, je le vois maintenant. Comme le .bind()obscurcit le nouveau value, je pense que je pourrais choisir de prolonger la fonction pour plus de lisibilité. Et désolé si je suis épais, mais si promiseForet promiseWhilene coexistent pas, alors comment l'un appelle-t-il l'autre?
Roamer-1888

2
@herve Vous pouvez essentiellement l'omettre et le remplacer return …par return Promise.resolve(…). Si vous avez besoin de garanties supplémentaires contre conditionou de actionlancer une exception (comme le Promise.methodfournit ), enveloppez tout le corps de la fonction dans unreturn Promise.resolve().then(() => { … })
Bergi

2
@herve En fait, cela devrait être Promise.resolve().then(action).…ou Promise.resolve(action()).…, vous n'avez pas besoin d'encapsuler la valeur de retour dethen
Bergi

134

Si vous voulez vraiment une promiseWhen()fonction générale à cette fin et à d'autres, alors faites-le par tous les moyens en utilisant les simplifications de Bergi. Cependant, en raison de la manière dont les promesses fonctionnent, passer des rappels de cette manière est généralement inutile et vous oblige à sauter à travers de petits obstacles complexes.

Autant que je sache, vous essayez:

  • pour récupérer de manière asynchrone une série de détails utilisateur pour une collection d'adresses e-mail (du moins, c'est le seul scénario qui a du sens).
  • pour ce faire en construisant une .then()chaîne par récursivité.
  • pour conserver l'ordre d'origine lors du traitement des résultats renvoyés.

Défini ainsi, le problème est en fait celui discuté sous «La Collection Kerfuffle» dans Promise Anti-patterns , qui propose deux solutions simples:

  • appels asynchrones parallèles utilisant Array.prototype.map()
  • appels asynchrones série utilisant Array.prototype.reduce().

L'approche parallèle donnera (carrément) le problème que vous essayez d'éviter - que l'ordre des réponses est incertain. L'approche sérielle construira la .then()chaîne requise - plate - pas de récursivité.

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

Appelez comme suit:

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

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

Comme vous pouvez le voir, il n'y a pas besoin de la vilaine var externe countou de sa conditionfonction associée . La limite (de 10 dans la question) est entièrement déterminée par la longueur du tableau arrayOfEmailAddys.


16
pense que cela devrait être la réponse choisie. approche gracieuse et très réutilisable.
ken

1
Est-ce que quelqu'un sait si une capture se propagerait vers le parent? Par exemple, si db.getUser devait échouer, l'erreur (de rejet) se propagerait-elle de nouveau?
wayofthefuture

@wayofthefuture, non. Pensez-y de cette façon ..... vous ne pouvez pas changer l'histoire.
Roamer-1888

4
Merci d'avoir répondu. Cela devrait être la réponse acceptée.
klvs

1
@ Roamer-1888 Mon erreur, j'ai mal lu la question initiale. Je cherchais (personnellement) une solution où la liste initiale dont vous avez besoin pour réduire augmente au fur et à mesure que vos demandes se règlent (c'est une requêtePlus d'une base de données). Dans ce cas, j'ai trouvé l'idée d'utiliser réduire avec un générateur une séparation assez sympa entre (1) l'extension conditionnelle de la chaîne de promesse et (2) la consommation des résultats retournés.
jhp

40

Voici comment je le fais avec l'objet standard Promise.

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

Great answer @youngwerth
Jam Risser

3
comment envoyer des paramètres de cette manière?
Akash khan

4
@khan sur la ligne chain = chain.then (func), vous pouvez faire soit: chain = chain.then(func.bind(null, "...your params here")); ou chain = chain.then(() => func("your params here"));
youngwerth

9

Donné

  • fonction asyncFn
  • tableau d'articles

Obligatoire

  • promettre de chaîner les .then () en série (dans l'ordre)
  • natif es6

Solution

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
Si asyncest sur le point de devenir un mot réservé en JavaScript, il peut être plus clair de renommer cette fonction ici.
hippietrail

De plus, n'est-il pas vrai que la grosse flèche fonctionne sans corps entre accolades renvoyant simplement à ce que l'expression évalue? Cela rendrait le code plus concis. Je pourrais également ajouter un commentaire indiquant qu'il currentn'est pas utilisé.
hippietrail

2
c'est la bonne manière!
teleme.io

4

Il existe une nouvelle façon de résoudre ce problème et c'est en utilisant async / await.

async function myFunction() {
  while(/* my condition */) {
    const res = await db.getUser(email);
    logger.log(res);
  }
}

myFunction().then(() => {
  /* do other stuff */
})

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function https://ponyfoo.com/articles/understanding-javascript-async-await


Merci, cela n'implique pas l'utilisation d'un framework (bluebird).
Rolf

3

La fonction suggérée par Bergi est vraiment sympa:

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

Je veux quand même faire un petit ajout, ce qui a du sens, lors de l'utilisation des promesses:

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

De cette façon, la boucle while peut être intégrée dans une chaîne de promesses et se résout avec lastValue (également si l'action () n'est jamais exécutée). Voir exemple:

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

Je ferais quelque chose comme ça:

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

de cette façon, dataAll est un tableau ordonné de tous les éléments à consigner. Et l'opération de journalisation s'effectuera lorsque toutes les promesses seront faites.


Promise.all appellera les promesses d'appels en même temps. L'ordre d'achèvement pourrait donc changer. La question demande des promesses enchaînées. L'ordre d'achèvement ne doit donc pas être modifié.
canbax

Edit 1: Vous n'avez pas du tout besoin d'appeler Promise.all. Tant que les promesses sont tenues, elles seront exécutées en parallèle.
canbax

1

Utilisez async et wait (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

Que diriez-vous de celui-ci en utilisant BlueBird ?

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

0

Voici une autre méthode (ES6 avec promesse std). Utilise les critères de sortie de type lodash / underscore (return === false). Notez que vous pouvez facilement ajouter une méthode exitIf () dans les options à exécuter dans 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

Utilisation de l'objet de promesse standard et avoir la promesse renvoyer les résultats.

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

Commencez par prendre un tableau de promesses (tableau de promesses) et après résolvez ces tableaux de promesses en utilisant 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
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.