Knoten JS Promise.all und forEach

120

Ich habe eine Array-ähnliche Struktur, die asynchrone Methoden verfügbar macht. Die asynchrone Methode ruft Rückgabearraystrukturen auf, die wiederum mehr asynchrone Methoden verfügbar machen. Ich erstelle ein weiteres JSON-Objekt, um die aus dieser Struktur erhaltenen Werte zu speichern. Daher muss ich vorsichtig sein, um Referenzen in Rückrufen zu verfolgen.

Ich habe eine Brute-Force-Lösung codiert, möchte aber eine idiomatischere oder sauberere Lösung lernen.

  1. Das Muster sollte für n Verschachtelungsebenen wiederholbar sein.
  2. Ich muss versprechen.all oder eine ähnliche Technik verwenden, um zu bestimmen, wann die einschließende Routine gelöst werden soll.
  3. Nicht jedes Element erfordert zwangsläufig einen asynchronen Aufruf. In einem verschachtelten Versprechen kann ich meinen JSON-Array-Elementen nicht einfach anhand des Index Zuweisungen vornehmen. Trotzdem muss ich so etwas wie versprechen.all im verschachtelten forEach verwenden, um sicherzustellen, dass alle Eigenschaftszuweisungen vor dem Auflösen der umschließenden Routine vorgenommen wurden.
  4. Ich benutze das Bluebird-Versprechen lib, aber dies ist keine Voraussetzung

Hier ist ein Teilcode -

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
quelle
Dies ist der Link zu der Arbeitsquelle, die ich verbessern möchte. github.com/pebanfield/change-view-service/blob/master/src/…
user3205931
1
Ich sehe in der Probe Sie drossel verwenden, drossel tatsächlich macht Ihnen das Leben noch einfacher mit Promise.map(gleichzeitige) und Promise.each(sequentiell) in diesem Fall auch zur Kenntnis Promise.deferveraltet - der Code in meiner Antwort zeigt , wie es zu vermeiden , indem der Rückkehr verspricht. Bei Versprechungen dreht sich alles um Rückgabewerte.
Benjamin Gruenbaum

Antworten:

367

Es ist ziemlich einfach mit einigen einfachen Regeln:

  • Wenn Sie ein Versprechen in a erstellen then, geben Sie es zurück - auf jedes Versprechen, das Sie nicht zurückgeben, wird nicht draußen gewartet.
  • Wenn Sie mehrere Versprechen erstellen, werden .alldiese - auf diese Weise werden alle Versprechen gewartet und kein Fehler von ihnen zum Schweigen gebracht.
  • Wann immer Sie thens nisten , können Sie normalerweise in der Mitte zurückkehren - thenKetten sind normalerweise höchstens 1 Ebene tief.
  • Wann immer Sie IO ausführen, sollte es mit einem Versprechen sein - entweder sollte es in einem Versprechen sein oder es sollte ein Versprechen verwenden, um seinen Abschluss zu signalisieren.

Und einige Tipps:

  • Das Zuordnen ist besser .mapals mitfor/push - Wenn Sie Werte mit einer Funktion mapzuordnen , können Sie den Gedanken, Aktionen einzeln anzuwenden und die Ergebnisse zu aggregieren, präzise ausdrücken.
  • Parallelität ist besser als sequentielle Ausführung, wenn sie kostenlos ist - es ist besser, Dinge gleichzeitig auszuführen und darauf zu warten, Promise.allals Dinge nacheinander auszuführen - jede wartet vor der nächsten.

Ok, also fangen wir an:

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
quelle
5
Ah, einige Regeln aus Ihrer Sicht :-)
Bergi
1
@Bergi jemand sollte wirklich eine Liste dieser Regeln und einen kurzen Hintergrund über Versprechen machen. Wir können es wahrscheinlich auf bluebirdjs.com hosten.
Benjamin Gruenbaum
da ich nicht nur danke sagen soll - dieses Beispiel sieht gut aus und ich mag den Kartenvorschlag, aber was tun mit einer Sammlung von Objekten, bei denen nur einige asynchrone Methoden haben? (Mein Punkt 3 oben) Ich hatte die Idee, die Parsing-Logik für jedes Element in eine Funktion zu abstrahieren und sie dann entweder in der asynchronen Aufrufantwort auflösen zu lassen oder wo kein asynchroner Aufruf war, einfach aufzulösen. Ist das sinnvoll?
user3205931
Ich muss auch die Map-Funktion haben, um sowohl das JSON-Objekt, das ich erstelle, als auch das Ergebnis des asynchronen Aufrufs zurückzugeben, den ich machen muss, damit ich nicht sicher bin, wie ich das machen soll - schließlich muss das Ganze rekursiv sein, da ich ein Verzeichnis gehe Struktur - Ich kaue immer noch daran, aber bezahlte Arbeit steht im Weg :(
user3205931
2
@ user3205931 Versprechen sind eher einfach als einfach - das heißt, sie sind nicht so vertraut wie andere Dinge, aber wenn Sie sie einmal bearbeitet haben, sind sie viel besser zu verwenden. Bleib dran, du wirst es bekommen :)
Benjamin Gruenbaum
42

Hier ist ein einfaches Beispiel mit Reduzieren. Es wird seriell ausgeführt, behält die Einfügereihenfolge bei und erfordert kein 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());
}

Und benutze es so:

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

Wir haben es nützlich gefunden, einen optionalen Kontext in eine Schleife zu senden. Der Kontext ist optional und wird von allen Iterationen gemeinsam genutzt.

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

Ihre Versprechen-Funktion würde folgendermaßen aussehen:

function logItem(item, context) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            console.log(item);
            context.itemCount++;
            resolve();
        })
    });
}
Steven Spungin
quelle
Vielen Dank dafür - Ihre Lösung hat bei mir funktioniert, wo andere (einschließlich verschiedener npm-Bibliotheken) dies nicht getan haben. Hast du das auf npm veröffentlicht?
SamF
Danke dir. Die Funktion setzt voraus, dass alle Versprechen gelöst sind. Wie gehen wir mit abgelehnten Versprechungen um? Wie gehen wir mit erfolgreichen Versprechungen mit einem Wert um?
Oyalhi
@oyalhi Ich würde vorschlagen, den 'Kontext' zu verwenden und ein Array von abgelehnten Eingabeparametern hinzuzufügen, die dem Fehler zugeordnet sind. Dies ist wirklich ein Anwendungsfall, da einige alle verbleibenden Versprechen ignorieren möchten und andere nicht. Für den zurückgegebenen Wert können Sie auch einen ähnlichen Ansatz verwenden.
Steven Spungin
1

Ich hatte die gleiche Situation durchgemacht. Ich habe mit zwei Promise.All () gelöst.

Ich denke, es war eine wirklich gute Lösung, also habe ich sie auf npm veröffentlicht: https://www.npmjs.com/package/promise-foreach

Ich denke, Ihr Code wird so etwas sein

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
quelle
0

Um die vorgestellte Lösung zu ergänzen, wollte ich in meinem Fall mehrere Daten aus Firebase abrufen, um eine Liste der Produkte zu erhalten. So habe ich es gemacht:

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
quelle