Node JS Promise.all et forEach

121

J'ai un tableau comme une structure qui expose les méthodes asynchrones. La méthode async appelle des structures de tableau de retour qui à leur tour exposent plus de méthodes async. Je crée un autre objet JSON pour stocker les valeurs obtenues à partir de cette structure et je dois donc faire attention au suivi des références dans les rappels.

J'ai codé une solution de force brute, mais j'aimerais apprendre une solution plus idiomatique ou plus propre.

  1. Le modèle doit être répétable pour n niveaux d'imbrication.
  2. Je dois utiliser promise.all ou une technique similaire pour déterminer quand résoudre la routine englobante.
  3. Tous les éléments n'impliqueront pas nécessairement de faire un appel asynchrone. Donc, dans une promesse imbriquée.all, je ne peux pas simplement attribuer des éléments à mes éléments de tableau JSON en fonction de l'index. Néanmoins, je dois utiliser quelque chose comme promise.all dans le forEach imbriqué pour m'assurer que toutes les attributions de propriété ont été effectuées avant de résoudre la routine englobante.
  4. J'utilise la librairie bluebird promise mais ce n'est pas une exigence

Voici un code partiel -

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();
user3205931
la source
C'est le lien vers la source de travail que je souhaite améliorer. github.com/pebanfield/change-view-service/blob/master/src/…
user3205931
1
Je vois dans l'exemple que vous utilisez bluebird, bluebird vous rend la vie encore plus facile avec Promise.map(simultané) et Promise.each(séquentiel) dans ce cas, également la note Promise.deferest obsolète - le code dans ma réponse montre comment l'éviter en renvoyant des promesses. Les promesses concernent toutes les valeurs de retour.
Benjamin Gruenbaum

Réponses:

369

C'est assez simple avec quelques règles simples:

  • Chaque fois que vous créez une promesse dans un then, renvoyez-la - toute promesse que vous ne reviendrez pas ne sera pas attendue à l'extérieur.
  • Chaque fois que vous créez plusieurs promesses, .allelles - de cette façon, il attend toutes les promesses et aucune erreur de l'une d'entre elles ne sera réduite au silence.
  • Chaque fois que vous emboitez then, vous pouvez généralement revenir dans le milieu - les thenchaînes ont généralement au plus 1 niveau de profondeur.
  • Chaque fois que vous effectuez des EI, cela devrait être avec une promesse - soit cela devrait être dans une promesse ou il devrait utiliser une promesse pour signaler son achèvement.

Et quelques conseils:

  • Le mappage est mieux fait avec .mapqu'avecfor/push - si vous mappez des valeurs avec une fonction, mapvous permet d'exprimer de manière concise la notion d'application des actions une par une et d'agréger les résultats.
  • La concurrence est meilleure que l'exécution séquentielle si elle est gratuite - il vaut mieux exécuter les choses simultanément et les attendre Promise.allque d'exécuter les choses l'une après l'autre - chacune attendant avant la suivante.

Ok, alors commençons:

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
la source
5
Ah, quelques règles de votre point de vue :-)
Bergi
1
@Bergi quelqu'un devrait vraiment faire une liste de ces règles et un bref aperçu des promesses. Nous pouvons probablement l'héberger sur bluebirdjs.com.
Benjamin Gruenbaum
puisque je ne suis pas censé simplement dire merci - cet exemple semble bon et j'aime bien la suggestion de carte, cependant, que faire d'une collection d'objets où seuls certains ont des méthodes asynchrones? (Mon point 3 ci-dessus) J'avais l'idée d'abstraire la logique d'analyse pour chaque élément dans une fonction, puis de la résoudre soit sur la réponse à l'appel asynchrone, soit sur l'absence d'appel asynchrone. Cela a-t-il du sens?
user3205931
J'ai également besoin que la fonction map renvoie à la fois l'objet json que je construis et le résultat de l'appel asynchrone, je dois donc m'assurer de ne pas savoir comment faire cela non plus - enfin, tout doit être récursif puisque je marche dans un répertoire structure - Je suis toujours en train de mâcher ça mais le travail rémunéré me
gêne
2
Les promesses @ user3205931 sont simples, plutôt que faciles , c'est-à-dire qu'elles ne sont pas aussi familières que les autres choses, mais une fois que vous les avez faites, elles sont bien meilleures à utiliser. Accrochez-vous, vous l'aurez :)
Benjamin Gruenbaum
42

Voici un exemple simple d'utilisation de réduire. Il fonctionne en série, maintient l'ordre d'insertion et ne nécessite pas 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());
}

Et utilisez-le comme ceci:

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

Nous avons trouvé utile d'envoyer un contexte facultatif en boucle. Le contexte est facultatif et partagé par toutes les itérations.

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

Votre fonction de promesse ressemblerait à ceci:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}
Steven Spungin
la source
Merci pour cela - votre solution a fonctionné pour moi là où d'autres (y compris diverses bibliothèques npm) n'ont pas fonctionné. Avez-vous publié ceci sur npm?
SamF
Je vous remercie. La fonction suppose que toutes les promesses sont résolues. Comment gérons-nous les promesses rejetées? Aussi, comment gérons-nous les promesses réussies avec une valeur?
oyalhi
@oyalhi Je suggérerais d'utiliser le «contexte» et d'ajouter un tableau de paramètres d'entrées rejetées mappés à l'erreur. C'est vraiment par cas d'utilisation, car certains voudront ignorer toutes les promesses restantes et d'autres non. Pour la valeur renvoyée, vous pouvez également utiliser une approche similaire.
Steven Spungin
1

J'ai traversé la même situation. J'ai résolu en utilisant deux Promise.All ().

Je pense que c'était vraiment une bonne solution, alors je l'ai publiée sur npm: https://www.npmjs.com/package/promise-foreach

Je pense que votre code sera quelque chose comme ça

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
la source
0

Juste pour ajouter à la solution présentée, dans mon cas, je voulais récupérer plusieurs données de Firebase pour une liste de produits. Voici comment je l'ai fait:

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
la source