web-dev-qa-db-fra.com

Alors que boucle avec des promesses

Quelle serait la manière idiomatique de faire quelque chose comme une boucle while avec des promesses. Alors:

faire quelque chose si la condition est toujours là, recommencez puis faites autre chose.

dosomething.then(possilblydomoresomethings).then(finish)

Je l'ai fait de cette façon. Je me demandais s'il y avait des façons meilleures/plus idomatiques?

var q = require('q');

var index = 1;

var useless =  function(){
        var currentIndex = index;
        console.log(currentIndex)
        var deferred = q.defer();
        setTimeout(function(){
            if(currentIndex > 10)
                deferred.resolve(false);
            else deferred.resolve(true);
            },500);
        return deferred.promise;
    }

var control = function(cont){
        var deferred = q.defer();
        if(cont){
                index = index + 1;
                useless().then(control).then(function(){
                        deferred.resolve();
                    });
            }
         else deferred.resolve();
        return deferred.promise;
    }

var chain = useless().then(control).then(function(){console.log('done')});

Sortie: 1 2 3 4 5 6 7 8 9 10 11 terminé

69
Grummle

J'utiliserais un objet pour envelopper la valeur. De cette façon, vous pouvez avoir une propriété done pour indiquer à la boucle que vous avez terminé.

// fn should return an object like
// {
//   done: false,
//   value: foo
// }
function loop(promise, fn) {
  return promise.then(fn).then(function (wrapper) {
    return !wrapper.done ? loop(Q(wrapper.value), fn) : wrapper.value;
  });
}

loop(Q.resolve(1), function (i) {
  console.log(i);
  return {
    done: i > 10,
    value: i++
  };
}).done(function () {
  console.log('done');
});
19
juandopazo

Voici une fonction réutilisable qui, à mon avis, est assez claire.

var Q = require("q");

// `condition` is a function that returns a boolean
// `body` is a function that returns a promise
// returns a promise for the completion of the loop
function promiseWhile(condition, body) {
    var done = Q.defer();

    function loop() {
        // When the result of calling `condition` is no longer true, we are
        // done.
        if (!condition()) return done.resolve();
        // Use `when`, in case `body` does not return a promise.
        // When it completes loop again otherwise, if it fails, reject the
        // done promise
        Q.when(body(), loop, done.reject);
    }

    // Start running the loop in the next tick so that this function is
    // completely async. It would be unexpected if `body` was called
    // synchronously the first time.
    Q.nextTick(loop);

    // The promise
    return done.promise;
}


// Usage
var index = 1;
promiseWhile(function () { return index <= 11; }, function () {
    console.log(index);
    index++;
    return Q.delay(500); // arbitrary async
}).then(function () {
    console.log("done");
}).done();
58
Stuart K

C’est le moyen le plus simple que j’ai trouvé d’exprimer le modèle de base: vous définissez une fonction qui appelle la promesse, vérifie son résultat, puis s’appelle à nouveau ou se termine.

const doSomething = value =>
  new Promise(resolve => 
    setTimeout(() => resolve(value >= 5 ? 'ok': 'no'), 1000))

const loop = value =>
  doSomething(value).then(result => {
    console.log(value)
    if (result === 'ok') {
      console.log('yay')      
    } else {
      return loop(value + 1)
    }
  })

loop(1).then(() => console.log('all done!'))

Voir en action sur JSBin

Si vous utilisiez une promesse qui résout ou refuse, définissez then et catch au lieu d'utiliser une clause if.

Si vous aviez un tableau de promesses, il vous suffirait de changer loop pour changer ou faire apparaître la suivante à chaque fois.


EDIT: Voici une version qui utilise async/await, parce que nous sommes en 2018:

const loop = async value => {
  let result = null
  while (result != 'ok') {
    console.log(value)
    result = await doSomething(value)
    value = value + 1
  }
  console.log('yay')
}

Voir en action sur CodePen

Comme vous pouvez le constater, il utilise une boucle while normale et aucune récursivité.

25
lawrence

C'est pour bluebird pas q mais puisque vous n'avez pas mentionné q spécifiquement .. dans le document api bluebird, l'auteur mentionne le fait de renvoyer une fonction générant une promesse serait plus idiomatique que d'utiliser des données différées.

var Promise = require('bluebird');
var i = 0;

var counter = Promise.method(function(){
    return i++;
})

function getAll(max, results){
    var results = results || [];
    return counter().then(function(result){
        results.Push(result);
        return (result < max) ? getAll(max, results) : results
    })
}

getAll(10).then(function(data){
    console.log(data);
})
12
aarosil

Comme je ne peux pas commenter la réponse de Stuart K, je vais en ajouter un peu ici. D'après la réponse de Stuart K, vous pouvez résumer le concept de façon étonnamment simple: Réutiliser une promesse non tenue. Ce qu'il a c'est essentiellement:

  1. Créer une nouvelle instance d'une promesse différée
  2. Définissez la fonction que vous souhaitez appeler en boucle
  3. Dans cette fonction:
    1. Vérifiez si vous avez terminé. et quand vous avez résolu la promesse créée dans # 1 et la retourner.
    2. Si vous n'avez pas terminé, dites à Q d'utiliser la promesse existante et d'exécuter la fonction non remplie qui est la fonction "récursive", ou d'échouer si elle est décédée. Q.when (promesse, votre fonction, failFunction)
  4. Après avoir défini votre fonction, utilisez Q pour la déclencher pour la première fois avec Q.nextTick (votreFonction).
  5. Enfin, renvoyez votre nouvelle promesse à l'appelant (ce qui déclenchera le début du processus).

La réponse de Stuart est une solution plus générique, mais les bases sont géniales (une fois que vous avez compris comment cela fonctionne).

5
millebi

Ce motif est maintenant plus facilement appelé en utilisant q-flow . Un exemple, pour le problème ci-dessus:

var q = require('q');
require('q-flow');
var index = 1;
q.until(function() {
  return q.delay(500).then(function() {
    console.log(index++);
    return index > 10;
  });
}).done(function() {
  return console.log('done');
});
4
Joe Hildebrand

Voici une extension du prototype Promise pour imiter le comportement d'une boucle for. Il prend en charge les promesses ou les valeurs immédiates pour les sections d'initialisation, de condition, de corps de boucle et d'incrémentation. Il prend également en charge les exceptions, sans fuites de mémoire. Vous trouverez ci-dessous un exemple d'utilisation.

var Promise = require('promise');


// Promise.loop([properties: object]): Promise()
//
//  Execute a loop based on promises. Object 'properties' is an optional
//  argument with the following fields:
//
//  initialization: function(): Promise() | any, optional
//
//      Function executed as part of the initialization of the loop. If
//      it returns a promise, the loop will not begin to execute until
//      it is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  condition: function(): Promise(result: bool) | bool, optional
//
//      Condition evaluated in the beginning of each iteration of the
//      loop. The function should return a boolean value, or a promise
//      object that resolves with a boolean data value.
//
//      Any exception occurring during the evaluation of the condition
//      will finish the loop with a rejected promise. Similarly, it this
//      function returns a promise, and this promise is rejected, the
//      loop finishes right away with a rejected promise.
//
//      If no condition function is provided, an infinite loop is
//      executed.
//
//  body: function(): Promise() | any, optional
//
//      Function acting as the body of the loop. If it returns a
//      promise, the loop will not proceed until this promise is
//      resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
//  increment: function(): Promise() | any, optional
//
//      Function executed at the end of each iteration of the loop. If
//      it returns a promise, the condition of the loop will not be
//      evaluated again until this promise is resolved.
//
//      Any exception occurring in this function will finish the loop
//      with a rejected promise. Similarly, if this function returns a
//      promise, and this promise is reject, the loop finishes right
//      away with a rejected promise.
//
Promise.loop = function(properties)
{
    // Default values
    properties = properties || {};
    properties.initialization = properties.initialization || function() { };
    properties.condition = properties.condition || function() { return true; };
    properties.body = properties.body || function() { };
    properties.increment = properties.increment || function() { };

    // Start
    return new Promise(function(resolve, reject)
    {
        var runInitialization = function()
        {
            Promise.resolve().then(function()
            {
                return properties.initialization();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runCondition = function()
        {
            Promise.resolve().then(function()
            {
                return properties.condition();
            })
            .then(function(result)
            {
                if (result)
                    process.nextTick(runBody);
                else
                    resolve();
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runBody = function()
        {
            Promise.resolve().then(function()
            {
                return properties.body();
            })
            .then(function()
            {
                process.nextTick(runIncrement);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        var runIncrement = function()
        {
            Promise.resolve().then(function()
            {
                return properties.increment();
            })
            .then(function()
            {
                process.nextTick(runCondition);
            })
            .catch(function(error)
            {
                reject(error);
            });
        }

        // Start running initialization
        process.nextTick(runInitialization);
    });
}


// Promise.delay(time: double): Promise()
//
//  Returns a promise that resolves after the given delay in seconds.
//
Promise.delay = function(time)
{
    return new Promise(function(resolve)
    {
        setTimeout(resolve, time * 1000);
    });
}


// Example
var i;
Promise.loop({
    initialization: function()
    {
        i = 2;
    },
    condition: function()
    {
        return i < 6;
    },
    body: function()
    {
        // Print "i"
        console.log(i);

        // Exception when 5 is reached
        if (i == 5)
            throw Error('Value of "i" reached 5');

        // Wait 1 second
        return Promise.delay(1);
    },
    increment: function()
    {
        i++;
    }
})
.then(function()
{
    console.log('LOOP FINISHED');
})
.catch(function(error)
{
    console.log('EXPECTED ERROR:', error.message);
});
3
user3707531
var Q = require('q')

var vetor  = ['a','b','c']

function imprimeValor(elements,initValue,defer){

    console.log( elements[initValue++] )
    defer.resolve(initValue)
    return defer.promise
}

function Qloop(initValue, elements,defer){

    Q.when( imprimeValor(elements, initValue, Q.defer()), function(initValue){

        if(initValue===elements.length){
            defer.resolve()
        }else{
            defer.resolve( Qloop(initValue,elements, Q.defer()) )
        }
    }, function(err){

        defer.reject(err)
    })

    return defer.promise
}

Qloop(0, vetor,Q.defer())
1
Lucas Rocha

J'utilise maintenant ceci:

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

Cela accepte un tableau arr et une fonction work et renvoie un Promise. La fonction fournie est appelée une fois pour chaque élément du tableau et passe à l'élément en cours et à son index dans le tableau. Il peut être synchrone ou asynchrone, auquel cas il doit renvoyer une promesse.

Vous pouvez l'utiliser comme ceci:

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
    // this could simply be sync, but can also be async
    // in which case it must return a Promise
    return new Promise(function(resolve){
        // use setTimeout to make this async
        setTimeout(function(){
            console.info(item, idx);
            resolve();
        }, 1000);
    });
})
.then(function(){
    console.info('DONE');
})
.catch(function(error){
    console.error('Failed', error);
})

Chaque élément du tableau sera traité à tour de rôle. Une fois que tous sont gérés, le code attribué à .then() s'exécutera ou, en cas d'erreur, le code attribué à .catch(). Dans la fonction work, vous pouvez throw et Error (dans le cas de fonctions synchrones) ou reject le Promise (dans le cas de fonctions synchrones) fonctions asynchrones) pour abandonner la boucle.

function each(arr, work) {
  function loop(arr, i) {
    return new Promise(function(resolve, reject) {
      if (i >= arr.length) {resolve();}
      else try {
        Promise.resolve(work(arr[i], i)).then(function() { 
          resolve(loop(arr, i+1))
        }).catch(reject);
      } catch(e) {reject(e);}
    });
  }
  return loop(arr, 0);
}

var items = ['Hello', 'cool', 'world'];
each(items, function(item, idx) {
  // this could simply be sync, but can also be async
  // in which case it must return a Promise
  return new Promise(function(resolve){
    // use setTimeout to make this async
    setTimeout(function(){
      console.info(item, idx);
      resolve();
    }, 1000);
  });
})
.then(function(){
  console.info('DONE');
})
.catch(function(error){
  console.error('Failed', error);
})
1
Stijn de Witt

Je pensais aussi bien pouvoir lancer mon chapeau dans le ring en utilisant ES6 Promises ...

function until_success(executor){
    var before_retry = undefined;
    var outer_executor = function(succeed, reject){
        var rejection_handler = function(err){
            if(before_retry){
                try {
                    var pre_retry_result = before_retry(err);
                    if(pre_retry_result)
                        return succeed(pre_retry_result);
                } catch (pre_retry_error){
                    return reject(pre_retry_error);
                }
            }
            return new Promise(executor).then(succeed, rejection_handler);                
        }
        return new Promise(executor).then(succeed, rejection_handler);
    }

    var outer_promise = new Promise(outer_executor);
    outer_promise.before_retry = function(func){
        before_retry = func;
        return outer_promise;
    }
    return outer_promise;
}

L'argument executor est le même que celui transmis à un constructeur Promise, mais sera appelé à plusieurs reprises jusqu'à ce qu'il déclenche le rappel de réussite. La fonction before_retry Permet la gestion d'erreur personnalisée lors des tentatives infructueuses. Si elle renvoie une valeur de vérité, elle sera considérée comme une forme de succès et la "boucle" se terminera, avec cette vérité comme résultat. Si aucune fonction before_retry N'est enregistrée ou renvoie une valeur de falsey, la boucle s'exécutera pour une autre itération. La troisième option est que la fonction before_retry Génère une erreur elle-même. Si cela se produit, la "boucle" se termine et transmet l'erreur en tant qu'erreur.


Voici un exemple:

var counter = 0;
function task(succ, reject){
    setTimeout(function(){
        if(++counter < 5)
            reject(counter + " is too small!!");
        else
            succ(counter + " is just right");
    }, 500); // simulated async task
}

until_success(task)
        .before_retry(function(err){
            console.log("failed attempt: " + err);
            // Option 0: return falsey value and move on to next attempt
            // return

            // Option 1: uncomment to get early success..
            //if(err === "3 is too small!!") 
            //    return "3 is sort of ok"; 

            // Option 2: uncomment to get complete failure..
            //if(err === "3 is too small!!") 
            //    throw "3rd time, very unlucky"; 
  }).then(function(val){
       console.log("finally, success: " + val);
  }).catch(function(err){
       console.log("it didn't end well: " + err);
  })

Sortie pour l'option 0:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
failed attempt: 4 is too small!!
finally, success: 5 is just right

Sortie pour l'option 1:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
finally, success: 3 is sort of ok

Sortie pour l'option 2:

failed attempt: 1 is too small!!
failed attempt: 2 is too small!!
failed attempt: 3 is too small!!
it didn't end well: 3rd time, very unlucky
0
dan-man

En utilisant la promesse ES6, je suis venu avec cela. Il enchaîne les promesses et retourne une promesse. Ce n'est pas techniquement une boucle while, mais montre comment itérer de manière synchrone sur des promesses.

function chain_promises(list, fun) {
    return list.reduce(
        function (promise, element) {
            return promise.then(function () {
                // I only needed to kick off some side-effects. If you need to get
                // a list back, you would append to it here. Or maybe use
                // Array.map instead of Array.reduce.
                fun(element);
            });
        },
        // An initial promise just starts things off.
        Promise.resolve(true)
    );
}

// To test it...

function test_function (element) {
    return new Promise(function (pass, _fail) {
        console.log('Processing ' + element);
        pass(true);
    });
}

chain_promises([1, 2, 3, 4, 5], test_function).then(function () {
    console.log('Done.');
});

Voici mon violon.

0
mqsoh

Beaucoup de réponses ici et ce que vous essayez de réaliser n’est pas très pratique. mais cela devrait fonctionner. Ceci a été implémenté dans une fonction aws lambda, avec Node.js 10, il ira jusqu'au délai d'attente de la fonction. Il peut également consommer une quantité décente de mémoire.

exports.handler = async (event) => {
  let res = null;
  while (true) {
    try{
     res = await dopromise();
    }catch(err){
     res = err;
    }
    console.log(res);
   }//infinite will time out
  };

  function dopromise(){
   return new Promise((resolve, reject) => {
    //do some logic
    //if error reject
        //reject('failed');
    resolve('success');
  });
}

Testé sur lambda et en bon état pendant plus de 5 min. Mais comme l'ont dit d'autres personnes, ce n'est pas une bonne chose à faire.

0
Hans-Eric Lippke