Node JS Promise.all y forEach

120

Tengo una estructura similar a una matriz que expone métodos asincrónicos. Las llamadas al método asíncrono devuelven estructuras de matriz que, a su vez, exponen más métodos asíncronos. Estoy creando otro objeto JSON para almacenar valores obtenidos de esta estructura, por lo que debo tener cuidado al realizar un seguimiento de las referencias en las devoluciones de llamada.

He codificado una solución de fuerza bruta, pero me gustaría aprender una solución más idiomática o limpia.

  1. El patrón debe ser repetible para n niveles de anidamiento.
  2. Necesito usar promise.all o alguna técnica similar para determinar cuándo resolver la rutina adjunta.
  3. No todos los elementos implicarán necesariamente realizar una llamada asíncrona. Entonces, en una promesa anidada, no puedo simplemente hacer asignaciones a los elementos de mi matriz JSON en función de index. Sin embargo, necesito usar algo como promise.all en el forEach anidado para asegurarme de que todas las asignaciones de propiedades se hayan realizado antes de resolver la rutina adjunta.
  4. Estoy usando la biblioteca de promesa bluebird pero esto no es un requisito

Aquí hay un código parcial:

var jsonItems = [];

items.forEach(function(item){

  var jsonItem = {};
  jsonItem.name = item.name;
  item.getThings().then(function(things){
  // or Promise.all(allItemGetThingCalls, function(things){

    things.forEach(function(thing, index){

      jsonItems[index].thingName = thing.name;
      if(thing.type === 'file'){

        thing.getFile().then(function(file){ //or promise.all?

          jsonItems[index].filesize = file.getSize();
usuario3205931
fuente
Este es el enlace a la fuente de trabajo que quiero mejorar. github.com/pebanfield/change-view-service/blob/master/src/…
user3205931
1
Veo en la muestra que está usando bluebird, bluebird en realidad hace su vida aún más fácil con Promise.map(concurrente) y Promise.each(secuencial) en este caso, también tenga en cuenta que Promise.deferestá obsoleto: el código en mi respuesta muestra cómo evitarlo devolviendo promesas. Las promesas tienen que ver con los valores de retorno.
Benjamin Gruenbaum

Respuestas:

368

Es bastante sencillo con algunas reglas simples:

  • Siempre que cree una promesa en a then, devuélvala ; cualquier promesa que no devuelva no se esperará afuera.
  • Siempre que crea varias promesas, .allellas , de esa manera, espera todas las promesas y no se silencia ningún error de ninguna de ellas.
  • Siempre que anide thens, normalmente puede volver en el medio ; las thencadenas suelen tener como máximo 1 nivel de profundidad.
  • Siempre que realice IO, debe ser con una promesa , o debe ser una promesa o debe usar una promesa para señalar su finalización.

Y algunos consejos:

  • El mapeo se hace mejor con .mapque confor/push - si está mapeando valores con una función, le mappermite expresar de manera concisa la noción de aplicar acciones una por una y agregar los resultados.
  • La simultaneidad es mejor que la ejecución secuencial si es gratis ; es mejor ejecutar las cosas al mismo tiempo y esperarlas Promise.allque ejecutar las cosas una tras otra, cada una esperando antes de la siguiente.

Ok, comencemos:

var items = [1, 2, 3, 4, 5];
var fn = function asyncMultiplyBy2(v){ // sample async action
    return new Promise(resolve => setTimeout(() => resolve(v * 2), 100));
};
// map over forEach since it returns

var actions = items.map(fn); // run the function over all items

// we now have a promises array and we want to wait for it

var results = Promise.all(actions); // pass array of promises

results.then(data => // or just .then(console.log)
    console.log(data) // [2, 4, 6, 8, 10]
);

// we can nest this of course, as I said, `then` chains:

var res2 = Promise.all([1, 2, 3, 4, 5].map(fn)).then(
    data => Promise.all(data.map(fn))
).then(function(data){
    // the next `then` is executed after the promise has returned from the previous
    // `then` fulfilled, in this case it's an aggregate promise because of 
    // the `.all` 
    return Promise.all(data.map(fn));
}).then(function(data){
    // just for good measure
    return Promise.all(data.map(fn));
});

// now to get the results:

res2.then(function(data){
    console.log(data); // [16, 32, 48, 64, 80]
});
Benjamin Gruenbaum
fuente
5
Ah, algunas reglas desde tu perspectiva :-)
Bergi
1
@Bergi, alguien debería hacer una lista de estas reglas y un breve resumen de las promesas. Probablemente podamos alojarlo en bluebirdjs.com.
Benjamin Gruenbaum
ya que se supone que no debo simplemente dar las gracias, este ejemplo se ve bien y me gusta la sugerencia del mapa, sin embargo, ¿qué hacer con una colección de objetos donde solo algunos tienen métodos asíncronos? (Mi punto 3 anterior) Tenía la idea de que abstraería la lógica de análisis para cada elemento en una función y luego haría que se resolviera en la respuesta de llamada asíncrona o donde no hubiera una llamada asíncrona, simplemente resolviera. ¿Tiene sentido?
user3205931
También necesito que la función de mapa devuelva tanto el objeto json que estoy construyendo como el resultado de la llamada asíncrona que necesito hacer, así que tampoco estoy seguro de cómo hacerlo; finalmente, todo debe ser recursivo ya que estoy caminando por un directorio estructura: todavía estoy masticando esto, pero el trabajo remunerado se interpone en el camino :(
user3205931
2
Las promesas de @ user3205931 son simples, en lugar de fáciles , es decir, no son tan familiares como otras cosas, pero una vez que las asimila, es mucho mejor usarlas. Agárrate fuerte, lo conseguirás :)
Benjamin Gruenbaum
42

Aquí hay un ejemplo simple usando reducir. Se ejecuta en serie, mantiene el orden de inserción y no requiere Bluebird.

/**
 * 
 * @param items An array of items.
 * @param fn A function that accepts an item from the array and returns a promise.
 * @returns {Promise}
 */
function forEachPromise(items, fn) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item);
        });
    }, Promise.resolve());
}

Y utilícelo así:

var items = ['a', 'b', 'c'];

function logItem(item) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            resolve();
        })
    });
}

forEachPromise(items, logItem).then(() => {
    console.log('done');
});

Nos ha resultado útil enviar un contexto opcional en bucle. El contexto es opcional y compartido por todas las iteraciones.

function forEachPromise(items, fn, context) {
    return items.reduce(function (promise, item) {
        return promise.then(function () {
            return fn(item, context);
        });
    }, Promise.resolve());
}

Su función de promesa se vería así:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}
Steven Spungin
fuente
Gracias por esto, su solución me ha funcionado donde otros (incluidas varias bibliotecas npm) no lo han hecho. ¿Tienes esto publicado en npm?
SamF
Gracias. La función asume que todas las promesas están resueltas. ¿Cómo manejamos las promesas rechazadas? Además, ¿cómo manejamos las promesas exitosas con valor?
oyalhi
@oyalhi Sugeriría usar el 'contexto' y agregar una matriz de parámetros de entrada rechazados asignados al error. Esto es realmente por caso de uso, ya que algunos querrán ignorar todas las promesas restantes y otros no. Para el valor devuelto, también puede utilizar un enfoque similar.
Steven Spungin
1

Pasé por la misma situación. Resolví usando dos Promise.All ().

Creo que fue una solución realmente buena, así que la publiqué en npm: https://www.npmjs.com/package/promise-foreach

Creo que tu código será algo como esto.

var promiseForeach = require('promise-foreach')
var jsonItems = [];
promiseForeach.each(jsonItems,
    [function (jsonItems){
        return new Promise(function(resolve, reject){
            if(jsonItems.type === 'file'){
                jsonItems.getFile().then(function(file){ //or promise.all?
                    resolve(file.getSize())
                })
            }
        })
    }],
    function (result, current) {
        return {
            type: current.type,
            size: jsonItems.result[0]
        }
    },
    function (err, newList) {
        if (err) {
            console.error(err)
            return;
        }
        console.log('new jsonItems : ', newList)
    })
Saulsluz
fuente
0

Solo para agregar a la solución presentada, en mi caso, quería obtener varios datos de Firebase para obtener una lista de productos. Así es como lo hice:

useEffect(() => {
  const fn = p => firebase.firestore().doc(`products/${p.id}`).get();
  const actions = data.occasion.products.map(fn);
  const results = Promise.all(actions);
  results.then(data => {
    const newProducts = [];
    data.forEach(p => {
      newProducts.push({ id: p.id, ...p.data() });
    });
    setProducts(newProducts);
  });
}, [data]);
Charles de Dreuille
fuente