Forma correcta de escribir bucles para promesas.


116

¿Cómo construir correctamente un bucle para asegurarse de que la siguiente llamada de promesa y el logger.log (res) encadenado se ejecuten sincrónicamente a través de la iteración? (azulejo)

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

Intenté de la siguiente manera (método 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');
}); 

Aunque parece funcionar, no creo que garantice el orden de llamada a logger.log (res);

¿Alguna sugerencia?


1
El código me parece bien (la recursividad con la loopfunción es la forma de hacer bucles sincrónicos). ¿Por qué crees que no hay garantía?
hugomg

Se garantiza que db.getUser (email) se llamará en orden. Pero, dado que db.getUser () en sí mismo es una promesa, llamarlo secuencialmente no significa necesariamente que las consultas de la base de datos para 'correo electrónico' se ejecuten secuencialmente debido a la característica asincrónica de la promesa. Por lo tanto, se invoca logger.log (res) dependiendo de qué consulta termine primero.
user2127480

1
@ user2127480: Pero la siguiente iteración del ciclo se llama secuencialmente solo después de que la promesa se haya resuelto, ¿así es como funciona ese whilecódigo?
Bergi

Respuestas:


78

No creo que garantice el orden de llamada a logger.log (res);

De hecho, lo hace. Esa declaración se ejecuta antes de la resolvellamada.

¿Alguna sugerencia?

Un montón. El más importante es el uso del antipatrón create-promise-manualmente , solo hazlo

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

En segundo lugar, esa whilefunción podría simplificarse mucho:

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

En tercer lugar, no usaría un whilebucle (con una variable de cierre) sino un forbucle:

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
¡Ups! Excepto que actiontoma valuecomo argumento promiseFor. SO no me dejaría hacer una edición tan pequeña. Gracias, es muy útil y elegante.
Gordon

1
@ Roamer-1888: Quizás la terminología sea un poco extraña, pero quiero decir que un whilebucle prueba algún estado global mientras que un forbucle tiene su variable de iteración (contador) vinculada al cuerpo del bucle. De hecho, he usado un enfoque más funcional que se parece más a una iteración de punto fijo que a un bucle. Verifique su código nuevamente, el valueparámetro es diferente.
Bergi

2
OK, lo veo ahora. Como el .bind()ofusca lo nuevo value, creo que podría optar por escribir la función para facilitar la lectura. Y lo siento si estoy siendo gordo pero si promiseFory promiseWhileno coexistimos, ¿cómo se llama uno al otro?
Roamer-1888

2
@herve Básicamente puedes omitirlo y reemplazar el return …por return Promise.resolve(…). Si necesita salvaguardas adicionales contra conditiono actionlanzar una excepción (como la Promise.methodproporciona ), envuelva todo el cuerpo de la función en unreturn Promise.resolve().then(() => { … })
Bergi

2
@herve En realidad, debería ser Promise.resolve().then(action).…o Promise.resolve(action()).…, no es necesario ajustar el valor de retorno dethen
Bergi

134

Si realmente desea una promiseWhen()función general para este y otros propósitos, entonces hágalo utilizando las simplificaciones de Bergi. Sin embargo, debido a la forma en que funcionan las promesas, pasar las devoluciones de llamada de esta manera generalmente es innecesario y te obliga a atravesar pequeños aros complejos.

Por lo que puedo decir, lo estás intentando:

  • para obtener de forma asincrónica una serie de detalles de usuario para una colección de direcciones de correo electrónico (al menos, ese es el único escenario que tiene sentido).
  • para hacerlo construyendo una .then()cadena mediante recursividad.
  • para mantener el orden original al manejar los resultados devueltos.

Así definido, el problema es en realidad el que se analiza en "The Collection Kerfuffle" en Promise Anti-patterns , que ofrece dos soluciones simples:

  • llamadas asincrónicas paralelas usando Array.prototype.map()
  • llamadas asincrónicas seriales usando Array.prototype.reduce().

El enfoque paralelo le dará (directamente) el problema que está tratando de evitar: que el orden de las respuestas es incierto. El enfoque en serie construirá la .then()cadena requerida , plana, sin recursividad.

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

Llame de la siguiente manera:

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

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

Como puede ver, no hay necesidad de la var externa fea counto su conditionfunción asociada . El límite (de 10 en la pregunta) está determinado completamente por la longitud de la matriz arrayOfEmailAddys.


16
siente que esta debería ser la respuesta seleccionada. enfoque elegante y muy reutilizable.
Ken

1
¿Alguien sabe si una captura se propagaría a los padres? Por ejemplo, si db.getUser fallara, ¿el error (de rechazo) se propagaría de nuevo?
wayofthefuture

@wayofthefuture, no. Piénselo de esta manera ... no puede cambiar la historia.
Roamer-1888

4
Gracias por la respuesta. Esta debería ser la respuesta aceptada.
klvs

1
@ Roamer-1888 Mi error, leí mal la pregunta original. Yo (personalmente) estaba buscando una solución en la que la lista inicial que necesita para reducir está creciendo a medida que se resuelven sus solicitudes (es una consulta más de una base de datos). En este caso, encontré la idea de usar reduce con un generador una separación bastante agradable de (1) la extensión condicional de la cadena de promesa y (2) el consumo de los resultados devueltos.
jhp

40

Así es como lo hago con el objeto Promise estándar.

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

Gran respuesta @youngwerth
Jam Risser

3
¿Cómo enviar parámetros de esta manera?
Akash khan

4
@khan en la línea chain = chain.then (func), puede hacer: chain = chain.then(func.bind(null, "...your params here")); o chain = chain.then(() => func("your params here"));
youngwerth

9

Dado

  • función asyncFn
  • variedad de elementos

Necesario

  • promesa de encadenamiento .entonces () está en serie (en orden)
  • nativo es6

Solución

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á a punto de convertirse en una palabra reservada en JavaScript, podría agregar claridad para cambiar el nombre de esa función aquí.
hippietrail

Además, ¿no es el caso de que las funciones de flecha gorda sin un cuerpo entre llaves simplemente devuelven lo que la expresión allí evalúa? Eso haría que el código fuera más conciso. También podría agregar un comentario que indique que currentno se usa.
hippietrail

2
esta es la forma correcta!
teleme.io


3

La función sugerida por Bergi es realmente agradable:

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

Aún así, quiero hacer una pequeña adición, que tiene sentido, cuando uso promesas:

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

De esta manera, el ciclo while se puede incrustar en una cadena de promesas y se resuelve con lastValue (también si la acción () nunca se ejecuta). Ver ejemplo:

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

Haría algo como esto:

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 esta manera, dataAll es una matriz ordenada de todos los elementos a registrar. Y la operación de registro se realizará cuando se cumplan todas las promesas.


Promise.all llamará a will call promises al mismo tiempo. Entonces, el orden de finalización podría cambiar. La pregunta pide promesas encadenadas. Por tanto, no se debe cambiar el orden de finalización.
canbax

Edición 1: no necesita llamar a Promise.all en absoluto. Mientras las promesas se realicen, se ejecutarán en paralelo.
canbax

1

Utilice async y 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

¿Qué tal este usando BlueBird ?

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

0

Aquí hay otro método (ES6 w / std Promise). Utiliza criterios de salida de tipo lodash / subrayado (return === falso). Tenga en cuenta que puede agregar fácilmente un método exitIf () en las opciones para ejecutar en 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

Usar el objeto de promesa estándar y hacer que la promesa devuelva los resultados.

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

Primero tome la matriz de promesas (matriz de promesas) y luego resuelva estas matrices de promesas 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
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.