Bucle while con promesas


Cuál sería la forma idiomática de hacer algo como un bucle while con promesas. Entonces:

Haz algo si la condición sigue en pie, hágalo de nuevo repetir entonces haz otra cosa.

dosomething.then(possilblydomoresomethings).then(finish)

Lo he hecho de esta manera Me preguntaba si había alguna manera mejor/más idomatic?

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

Salida: 1 2 3 4 5 6 7 8 9 10 11 hecho

Author: Grummle, 2013-06-20

12 answers

Usaría un objeto para envolver el valor. De esa manera puedes tener una propiedad done para que el bucle sepa que has terminado.

// 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');
});
 16
Author: juandopazo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-06-21 15:31:08

Aquí hay una función reutilizable que creo que es bastante clara.

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();
 57
Author: Stuart K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-06-21 15:18:30

Esta es la forma más sencilla que he encontrado para expresar el patrón básico: se define una función que llama a la promesa, comprueba su resultado, y luego se llama a sí misma de nuevo o termina.

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!'))

Ver en acción en JSBin

Si estuviera usando una promesa que resuelve o rechaza, definiría then y catch en lugar de usar una cláusula if.

Si tuvieras una serie de promesas, simplemente cambiarías loop para cambiar o hacer estallar la siguiente cada una tiempo.


EDITAR: Aquí hay una versión que usa async/await, porque es 2018:

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

Véalo en acción en CodePen

Como puede ver, utiliza un bucle while normal y sin recursión.

 18
Author: lawrence,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-04-03 23:54:13

Esto es para bluebird no q, pero ya que no mencionaste q específicamente.. en el documento de la api de bluebird, el autor menciona que devolver una función generadora de promesas sería más idiomático que usar deferreds.

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
Author: aarosil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-17 18:40:24

Como no puedo comentar la respuesta de Stuart K, agregaré un poco aquí. Basado en la respuesta de Stuart K, puedes reducirlo a un concepto sorprendentemente simple: Reutilizar una promesa incumplida. Lo que tiene es esencialmente:

  1. Crear una nueva instancia de una promesa diferida
  2. Defina la función a la que desea llamar en un bucle
  3. Dentro de esa función:
    1. Verifique si ha terminado; y cuando esté resuelto la promesa creada en el #1 y regrese se.
    2. Si no ha terminado, entonces dígale a Q que use la promesa existente y ejecute la función unfullfilled que es la función "recursiva", o falle si murió. Q. when (promise, yourFunction, failFunction)
  4. Después de definir su función use Q para activar la función por primera vez usando Q. nextTick (yourFunction)
  5. Finalmente devuelve tu nueva promesa a la persona que llama (lo que activará el inicio de todo).

La respuesta de Stuart es para un más solución genérica, pero los conceptos básicos son impresionantes (una vez que te das cuenta de cómo funciona).

 5
Author: millebi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-07 21:52:52

Este patrón ahora se llama más fácilmente usando q-flow. Un ejemplo, para el problema anterior:

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
Author: Joe Hildebrand,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-14 21:58:46

Aquí hay una extensión del prototipo Promise para imitar el comportamiento de un bucle for. Admite promesas o valores inmediatos para las secciones de inicialización, condición, cuerpo de bucle e incremento. También tiene soporte completo para excepciones, y no tiene fugas de memoria. A continuación se da un ejemplo sobre cómo usarlo.

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
Author: user3707531,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-25 20:44:18
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
Author: Lucas Rocha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-18 18:16:13

Ahora estoy usando esto: {[14]]}

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

Esto acepta un array arr y una función work y devuelve un Promise. La función suministrada se llama una vez para cada elemento en la matriz y se le pasa el elemento actual y su índice en la matriz. Puede ser sync o async, en cuyo caso debe devolver una Promesa.

Puedes usarlo así: {[14]]}

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

Cada elemento del array será manejado por turnos. Una vez que todos se manejan, el código dado a .then() se ejecutará, o, si algún error ocurrió, el código dado a .catch(). Dentro de la función work, puede throw un Error (en el caso de funciones síncronas) o reject el Promise (en el caso de funciones asíncronas) para abortar el bucle.

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
Author: Stijn de Witt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-18 19:01:28

Usando la Promesa ES6, se me ocurrió esto. Encadena las promesas y devuelve una promesa. Técnicamente no es un bucle while, pero muestra cómo iterar sobre promesas de forma sincrónica.

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

Aquí está mi violín.

 0
Author: mqsoh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-12 17:13:12

Pensé que también podría lanzar mi sombrero en el ring, usando Promesas ES6...

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

El argumento executor es el mismo que el pasado a un constructor Promise, pero se llamará repetidamente hasta que active la devolución de llamada de éxito. La función before_retry permite el manejo personalizado de errores en los intentos fallidos. Si devuelve un valor de verdad se considerará una forma de éxito y el" bucle " terminará, con ese valor de verdad como resultado. Si no se registra ninguna función before_retry, o devuelve un valor falsey, luego el bucle se ejecutará para otra iteración. La tercera opción es que la función before_retry arroje un error. Si esto sucede, entonces el" bucle " terminará, pasando ese error como un error.


He aquí un ejemplo:

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

Salida para la opción 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

Salida para la opción 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

Salida para la opción 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
Author: dan-man,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-06 16:51:45

Escribí un módulo que te ayuda a hacer bucles encadenados de tareas asíncronas con promesas, se basa en la respuesta anterior proporcionada por juandopazo

/**
 * Should loop over a task function which returns a "wrapper" object
 * until wrapper.done is true. A seed value wrapper.seed is propagated to the
 * next run of the loop.
 *
 * todo/maybe? Reject if wrapper is not an object with done and seed keys.
 *
 * @param {Promise|*} seed
 * @param {Function} taskFn
 *
 * @returns {Promise.<*>}
 */
function seedLoop(seed, taskFn) {
  const seedPromise = Promise.resolve(seed);

  return seedPromise
    .then(taskFn)
    .then((wrapper) => {
      if (wrapper.done) {
        return wrapper.seed;
      }

      return seedLoop(wrapper.seed, taskFn);
    });
}

// A super simple example of counting to ten, which doesn't even
// do anything asynchronous, but if it did, it should resolve to 
// a promise that returns the { done, seed } wrapper object for the
// next call of the countToTen task function.
function countToTen(count) {
  const done = count > 10;
  const seed = done ? count : count + 1;

  return {done, seed};
}

seedLoop(1, countToTen).then((result) => {
  console.log(result); // 11, the first value which was over 10.
});

Https://github.com/CascadeEnergy/promise-seedloop

 -1
Author: nackjicholson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-22 16:50:11