Wie greife ich auf frühere Versprechungsergebnisse in einer .then () -Kette zu?


650

Ich habe meinen Code in Versprechen umstrukturiert und eine wundervolle lange flache Versprechenskette aufgebaut , die aus mehreren .then()Rückrufen besteht. Am Ende möchte ich einen zusammengesetzten Wert zurückgeben und muss auf mehrere Zwischenversprechen zugreifen . Die Auflösungswerte aus der Mitte der Sequenz sind jedoch im letzten Rückruf nicht im Geltungsbereich. Wie kann ich darauf zugreifen?

function getExample() {
    return promiseA(…).then(function(resultA) {
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        return // How do I gain access to resultA here?
    });
}

2
Diese Frage ist wirklich interessant und selbst wenn sie markiert ist javascript, ist sie in einer anderen Sprache relevant. Ich benutze nur die Antwort "Break the Chain" in Java und jdeferred
Pontard

Antworten:


376

Frei kämpfen

Wenn Sie auf die Zwischenwerte in Ihrer Kette zugreifen müssen, sollten Sie Ihre Kette in die einzelnen Teile aufteilen, die Sie benötigen. Anstatt einen Rückruf anzuhängen und irgendwie zu versuchen, seinen Parameter mehrmals zu verwenden, fügen Sie mehrere Rückrufe demselben Versprechen hinzu - wo immer Sie den Ergebniswert benötigen. Vergessen Sie nicht, ein Versprechen repräsentiert nur (Proxies) einen zukünftigen Wert ! Verwenden Sie neben dem Ableiten eines Versprechens vom anderen in einer linearen Kette die Versprechen-Kombinatoren, die Sie von Ihrer Bibliothek erhalten, um den Ergebniswert zu erstellen.

Dies führt zu einem sehr einfachen Kontrollfluss, einer klaren Zusammensetzung der Funktionen und damit zu einer einfachen Modularisierung.

function getExample() {
    var a = promiseA(…);
    var b = a.then(function(resultA) {
        // some processing
        return promiseB(…);
    });
    return Promise.all([a, b]).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

Anstelle der Parameterzerstörung im Rückruf, die danach Promise.allerst mit ES6 verfügbar wurde, wurde der thenAufruf in ES5 durch eine raffinierte Hilfsmethode ersetzt, die von vielen Versprechensbibliotheken bereitgestellt wurde ( Q , Bluebird , when ,…) : .spread(function(resultA, resultB) { ….

Bluebird bietet auch eine spezielle joinFunktion , um diese Promise.all+ spreadKombination durch ein einfacheres (und effizienteres) Konstrukt zu ersetzen :


return Promise.join(a, b, function(resultA, resultB) {  });

1
Werden die Funktionen innerhalb des Arrays der Reihe nach ausgeführt?
Scaryguy

6
@scaryguy: Es gibt keine Funktionen im Array, das sind Versprechen. promiseAund promiseBsind die (Versprechen zurückgebenden) Funktionen hier.
Bergi

2
@ Roland Nie gesagt, es war :-) Diese Antwort wurde im ES5-Zeitalter geschrieben, in dem überhaupt keine Versprechungen im Standard waren, und spreadwar in diesem Muster sehr nützlich. Für modernere Lösungen siehe die akzeptierte Antwort. Ich habe jedoch bereits die explizite Passthrough-Antwort aktualisiert , und es gibt wirklich keinen guten Grund, diese nicht ebenfalls zu aktualisieren.
Bergi

1
@reify Nein, das solltest du nicht tun , es würde Probleme mit Ablehnungen bringen.
Bergi

1
Ich verstehe dieses Beispiel nicht. Wenn es eine Kette von 'dann'-Anweisungen gibt, die erfordern, dass Werte in der gesamten Kette verbreitet werden, sehe ich nicht, wie dies das Problem löst. Ein Versprechen, für das ein vorheriger Wert erforderlich ist, kann NICHT ausgelöst (erstellt) werden, bis dieser Wert vorhanden ist. Darüber hinaus wartet Promise.all () einfach darauf, dass alle Versprechen in seiner Liste abgeschlossen sind: Es wird kein Auftrag erteilt. Ich brauche also jede 'next'-Funktion, um auf alle vorherigen Werte zugreifen zu können, und ich sehe nicht, wie Ihr Beispiel das macht. Sie sollten uns durch Ihr Beispiel führen, weil ich es nicht glaube oder verstehe.
David Spector

237

ECMAScript Harmony

Natürlich wurde dieses Problem auch von den Sprachdesignern erkannt. Sie haben viel Arbeit geleistet und der Vorschlag für asynchrone Funktionen hat es endlich geschafft

ECMAScript 8

Sie benötigen keine einzige thenAufruf- oder Rückruffunktion mehr, da Sie in einer asynchronen Funktion (die beim Aufruf ein Versprechen zurückgibt) einfach darauf warten können, dass Versprechen direkt aufgelöst werden. Es enthält auch beliebige Kontrollstrukturen wie Bedingungen, Schleifen und Try-Catch-Klauseln, aber der Einfachheit halber brauchen wir sie hier nicht:

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

Während wir auf ES8 warteten, verwendeten wir bereits eine sehr ähnliche Syntax. ES6 wurde mit Generatorfunktionen geliefert , mit denen die Ausführung bei willkürlich platzierten yieldSchlüsselwörtern in Teile zerlegt werden kann . Diese Slices können unabhängig voneinander und sogar asynchron nacheinander ausgeführt werden - und genau das tun wir, wenn wir auf eine Versprechen-Lösung warten möchten, bevor wir den nächsten Schritt ausführen.

Es gibt dedizierte Bibliotheken (wie co oder task.js ), aber auch viele Versprechensbibliotheken verfügen über Hilfsfunktionen ( Q , Bluebird , when ,…), die diese asynchrone schrittweise Ausführung für Sie ausführen , wenn Sie ihnen eine Generatorfunktion geben gibt Versprechen.

var getExample = Promise.coroutine(function* () {
//               ^^^^^^^^^^^^^^^^^ Bluebird syntax
    var resultA = yield promiseA(…);
    // some processing
    var resultB = yield promiseB(…);
    // more processing
    return // something using both resultA and resultB
});

Dies funktionierte in Node.js seit Version 4.0, auch einige Browser (oder deren Entwicklungseditionen) unterstützten die Generatorsyntax relativ früh.

ECMAScript 5

Wenn Sie jedoch abwärtskompatibel sein möchten / müssen, können Sie diese nicht ohne Transpiler verwenden. Sowohl Generatorfunktionen als auch Async-Funktionen werden vom aktuellen Tool unterstützt, siehe zum Beispiel die Dokumentation von Babel zu Generatoren und Async-Funktionen .

Und dann gibt es noch viele andere Compile-to-JS-Sprachen , die sich der Vereinfachung der asynchronen Programmierung widmen. Sie verwenden in der Regel eine Syntax ähnlich wie await(zB Iced Coffeescript ), aber es gibt auch andere , die eine Haskell-ähnliche Funktion do-Notation (zB LatteJs , monadische , PURESCRIPT oder LispyScript ).


@Bergi Müssen Sie auf die asynchrone Funktion warten, um getExample () von außerhalb des Codes zu testen?
Arisalexis

@arisalexis: Ja, es getExamplehandelt sich immer noch um eine Funktion, die ein Versprechen zurückgibt und genau wie die Funktionen in den anderen Antworten funktioniert, jedoch mit einer besseren Syntax. Sie könnten awaiteine andere asyncFunktion aufrufen oder .then()mit dem Ergebnis verketten.
Bergi

1
Ich bin neugierig, warum haben Sie Ihre eigene Frage sofort beantwortet, nachdem Sie sie gestellt haben? Hier gibt es einige gute Diskussionen, aber ich bin neugierig. Vielleicht haben Sie Ihre Antworten selbst gefunden, nachdem Sie gefragt haben?
Granmoe

@granmoe: Ich habe die ganze Diskussion absichtlich als kanonisches Duplikat gepostet
Bergi

Gibt es eine (nicht zu mühsame) Möglichkeit, die Verwendung von Promise.coroutine (dh nicht Bluebird oder eine andere Bibliothek, sondern nur einfaches JS) im ECMAScript 6-Beispiel mit der Generatorfunktion zu vermeiden? Ich hatte so etwas im Sinn, steps.next().value.then(steps.next)...aber das hat nicht funktioniert.
Ein Lernender hat keinen Namen

102

Synchrone Inspektion

Zuweisen von Versprechungen für später benötigte Werte zu Variablen und anschließendes Abrufen ihres Werts durch synchrone Überprüfung. Das Beispiel verwendet die .value()Methode von bluebird, aber viele Bibliotheken bieten eine ähnliche Methode an.

function getExample() {
    var a = promiseA(…);

    return a.then(function() {
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // a is guaranteed to be fulfilled here so we can just retrieve its
        // value synchronously
        var aValue = a.value();
    });
}

Dies kann für beliebig viele Werte verwendet werden:

function getExample() {
    var a = promiseA(…);

    var b = a.then(function() {
        return promiseB(…)
    });

    var c = b.then(function() {
        return promiseC(…);
    });

    var d = c.then(function() {
        return promiseD(…);
    });

    return d.then(function() {
        return a.value() + b.value() + c.value() + d.value();
    });
}

6
Dies ist meine Lieblingsantwort: lesbar, erweiterbar und minimale Abhängigkeit von Bibliotheks- oder Sprachfunktionen
Jason

13
@ Jason: Äh, " minimale Abhängigkeit von Bibliotheksfunktionen "? Die synchrone Überprüfung ist eine Bibliotheksfunktion und eine nicht standardmäßige Funktion zum Booten.
Bergi

2
Ich denke, er meinte bibliotheksspezifische Funktionen
Deathgaze

54

Verschachtelung (und) Verschlüsse

Die Verwendung von Verschlüssen zur Aufrechterhaltung des Variablenumfangs (in unserem Fall der Parameter der Erfolgsrückruffunktion) ist die natürliche JavaScript-Lösung. Mit Versprechungen können wir Rückrufe willkürlich verschachteln und .then() reduzieren - sie sind semantisch äquivalent, mit Ausnahme des Umfangs des inneren.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

Natürlich baut dies eine Einrückungspyramide. Wenn die Einrückung zu groß wird, können Sie trotzdem die alten Werkzeuge anwenden, um der Pyramide des Untergangs entgegenzuwirken : Modularisieren, zusätzliche benannte Funktionen verwenden und die Versprechenskette reduzieren, sobald Sie keine Variable mehr benötigen.
Theoretisch können Sie immer mehr als zwei Verschachtelungsebenen vermeiden (indem Sie alle Verschlüsse explizit machen). In der Praxis können Sie so viele verwenden, wie sinnvoll sind.

function getExample() {
    // preprocessing
    return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
    return function(resultA) {
        // some processing
        return promiseB(…).then(makeBhandler(resultA, …));
    };
}
function makeBhandler(resultA, …) {
    return function(resultB) {
        // more processing
        return // anything that uses the variables in scope
    };
}

Sie können auch Hilfsfunktionen für diese Art der Teilanwendung verwenden , z. B. _.partialvon Underscore / lodash oder der nativen .bind()Methode , um die Einrückung weiter zu verringern:

function getExample() {
    // preprocessing
    return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
    // some processing
    return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
    // more processing
    return // anything that uses resultA and resultB
}

5
Derselbe Vorschlag wird als Lösung für 'Advanced error # 4' in Nolan Lawsons Artikel über Versprechen pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html gegeben . Es ist eine gute Lektüre.
Robert

2
Dies ist genau die bindFunktion in Monaden. Haskell bietet syntaktischen Zucker (Do-Notation), damit er wie eine asynchrone / wartende Syntax aussieht.
Zeronone

50

Expliziter Durchgang

Ähnlich wie beim Verschachteln der Rückrufe beruht diese Technik auf Schließungen. Die Kette bleibt jedoch flach - anstatt nur das neueste Ergebnis zu übergeben, wird für jeden Schritt ein Statusobjekt übergeben. Diese Statusobjekte sammeln die Ergebnisse der vorherigen Aktionen und geben alle Werte, die später erneut benötigt werden, sowie das Ergebnis der aktuellen Aufgabe weiter.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

Hier ist dieser kleine Pfeil b => [resultA, b]die Funktion, die geschlossen resultAwird und ein Array beider Ergebnisse an den nächsten Schritt übergibt. Womit die Syntax der Parameterdestrukturierung verwendet wird, um sie wieder in einzelne Variablen aufzuteilen.

Bevor die Destrukturierung mit ES6 verfügbar wurde, .spread()wurde von vielen Versprechensbibliotheken ( Q , Bluebird , when ,…) eine raffinierte Hilfsmethode bereitgestellt . Es ist erforderlich, dass eine Funktion mit mehreren Parametern - einer für jedes Array-Element - als verwendet wird .spread(function(resultA, resultB) { ….

Natürlich kann dieser hier benötigte Verschluss durch einige Hilfsfunktionen weiter vereinfacht werden, z

function addTo(x) {
    // imagine complex `arguments` fiddling or anything that helps usability
    // but you get the idea with this simple one:
    return res => [x, res];
}


return promiseB(…).then(addTo(resultA));

Alternativ können Sie Promise.alldas Versprechen für das Array erstellen:

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
                                                    // as if passed to Promise.resolve()
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

Und Sie können nicht nur Arrays verwenden, sondern auch beliebig komplexe Objekte. Zum Beispiel mit _.extendoder Object.assignin einer anderen Hilfsfunktion:

function augment(obj, name) {
    return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(augment({resultA}, "resultB"));
    }).then(function(obj) {
        // more processing
        return // something using both obj.resultA and obj.resultB
    });
}

Während dieses Muster eine flache Kette garantiert und explizite Zustandsobjekte die Klarheit verbessern können, wird es für eine lange Kette mühsam. Besonders wenn Sie den Staat nur sporadisch brauchen, müssen Sie ihn dennoch durch jeden Schritt führen. Mit dieser festen Schnittstelle sind die einzelnen Rückrufe in der Kette ziemlich eng gekoppelt und können sich nicht ändern. Dadurch wird das Ausklammern einzelner Schritte erschwert, und Rückrufe können nicht direkt von anderen Modulen bereitgestellt werden. Sie müssen immer in Code gekoppelt werden, der sich um den Status kümmert. Abstrakte Helferfunktionen wie die oben genannten können den Schmerz ein wenig lindern, sind aber immer vorhanden.


Erstens denke ich nicht, dass die Syntax, bei der das weggelassen wird Promise.all, gefördert werden sollte (es funktioniert in ES6 nicht, wenn die Destrukturierung es ersetzt und das Umschalten auf a .spreadzu thenoft unerwarteten Ergebnissen führt. Ich bin mir nicht sicher, warum Sie das brauchen Gebrauch Augmentation - Dinge zu dem Versprechen Prototyp Zugabe ist keine akzeptable Art und Weise ES6 Versprechungen sowieso , die mit (die derzeit nicht unterstützt) Subklassen erweitert werden sollen , zu verlängern.
Benjamin Grünbaum

@BenjaminGruenbaum: Was meinst du mit " Syntax weglassenPromise.all "? Keine der Methoden in dieser Antwort wird mit ES6 brechen. Der Wechsel von a spreadzu einer Destrukturierung thensollte ebenfalls keine Probleme haben. Re .prototype.augment: Ich wusste, dass jemand es bemerken würde, ich habe nur gern Möglichkeiten erkundet - ich werde es herausarbeiten.
Bergi

Mit der Array-Syntax meine ich, return [x,y]; }).spread(...stattdessen return Promise.all([x, y]); }).spread(...würde sich dies nicht ändern, wenn der Spread gegen es6-destrukturierenden Zucker getauscht wird, und es wäre auch kein seltsamer Randfall, in dem Versprechen zurückkehrende Arrays anders behandeln als alles andere.
Benjamin Gruenbaum

3
Dies ist wahrscheinlich die beste Antwort. Versprechen sind "Functional Reactive Programming" -Licht, und dies ist oft die verwendete Lösung. Zum Beispiel hat BaconJs #combineTemplate, mit dem Sie Ergebnisse zu einem Objekt kombinieren können, das in der Kette weitergegeben wird
U Avalos

1
@CapiEtheriel Die Antwort wurde geschrieben, als ES6 nicht so weit verbreitet war wie heute. Ja, vielleicht ist es Zeit, die Beispiele
auszutauschen

35

Veränderlicher Kontextzustand

Die triviale (aber unelegante und eher fehleranfällige) Lösung besteht darin, nur Variablen mit höherem Gültigkeitsbereich (auf die alle Rückrufe in der Kette Zugriff haben) zu verwenden und ihnen Ergebniswerte zu schreiben, wenn Sie sie erhalten:

function getExample() {
    var resultA;
    return promiseA(…).then(function(_resultA) {
        resultA = _resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both resultA and resultB
    });
}

Anstelle vieler Variablen kann auch ein (anfangs leeres) Objekt verwendet werden, auf dem die Ergebnisse als dynamisch erstellte Eigenschaften gespeichert werden.

Diese Lösung hat mehrere Nachteile:

  • Der veränderliche Zustand ist hässlich und globale Variablen sind böse .
  • Dieses Muster funktioniert nicht über Funktionsgrenzen hinweg. Die Modularisierung der Funktionen ist schwieriger, da ihre Deklarationen den gemeinsamen Bereich nicht verlassen dürfen
  • Der Umfang der Variablen verhindert nicht den Zugriff auf sie, bevor sie initialisiert werden. Dies ist besonders wahrscheinlich bei komplexen Versprechungskonstruktionen (Schleifen, Verzweigungen, Ausnahmen), bei denen Rennbedingungen auftreten können. Das explizite Übergeben von Status, ein deklaratives Design , das Ermutigung verspricht, erzwingt einen saubereren Codierungsstil, der dies verhindern kann.
  • Man muss den Bereich für diese gemeinsam genutzten Variablen richtig auswählen. Es muss lokal für die ausgeführte Funktion sein, um Race-Bedingungen zwischen mehreren parallelen Aufrufen zu verhindern, wie dies der Fall wäre, wenn beispielsweise der Status in einer Instanz gespeichert wurde.

Die Bluebird-Bibliothek empfiehlt die Verwendung eines weitergegebenen Objekts, indem mit ihrer bind()Methode ein Kontextobjekt einer Versprechen-Kette zugewiesen wird. Auf sie kann von jeder Rückruffunktion über das ansonsten unbrauchbare thisSchlüsselwort zugegriffen werden . Während Objekteigenschaften anfälliger für unentdeckte Tippfehler als für Variablen sind, ist das Muster ziemlich clever:

function getExample() {
    return promiseA(…)
    .bind({}) // Bluebird only!
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }).bind(); // don't forget to unbind the object if you don't want the
               // caller to access it
}

Dieser Ansatz kann leicht in Versprechungsbibliotheken simuliert werden, die .bind nicht unterstützen (obwohl etwas ausführlicher und nicht in einem Ausdruck verwendet werden kann):

function getExample() {
    var ctx = {};
    return promiseA(…)
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }.bind(ctx)).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }.bind(ctx));
}

.bind()ist nicht notwendig, um Speicherverlust zu verhindern
Esailija

@Esailija: Aber enthält das zurückgegebene Versprechen sonst keinen Verweis auf das Kontextobjekt? OK, natürlich wird die Speicherbereinigung später erledigt. Es ist kein "Leck", es sei denn, das Versprechen wird niemals abgegeben.
Bergi

Ja, aber Versprechen beziehen sich auch auf ihre Erfüllungswerte und Fehlergründe ... aber nichts bezieht sich auf das Versprechen, also spielt es keine Rolle
Esailija

4
Bitte teilen Sie diese Antwort in zwei Teile, da ich fast über die Präambel abgestimmt habe! Ich denke, "die triviale (aber unelegante und eher fehleranfällige) Lösung" ist die sauberste und einfachste Lösung, da sie nicht mehr auf Schließungen und veränderlichen Zuständen beruht als auf Ihrer akzeptierten Selbstantwort, sondern einfacher ist. Verschlüsse sind weder global noch böse. Die Argumente gegen diesen Ansatz ergeben für mich angesichts der Prämisse keinen Sinn. Welche Modularisierungsprobleme kann es bei einer "wunderbaren langen flachen Versprechenskette" geben?
Fock

2
Wie ich oben sagte, sind Versprechen "Functional Reactive Programming" -Licht. Dies ist ein Anti-Muster in FRP
U Avalos

15

Ein weniger harter Dreh zu "Mutable Contextual State"

Die Verwendung eines Objekts mit lokalem Gültigkeitsbereich zum Sammeln der Zwischenergebnisse in einer Versprechenskette ist ein vernünftiger Ansatz für die von Ihnen gestellte Frage. Betrachten Sie das folgende Snippet:

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(paramsA).then(function(resultA){
        results.a = resultA;
        return promiseB(paramsB);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(paramsC);
    }).then(function(resultC){
        //Resolve with composite of all promises
        return Promise.resolve(results.a + results.b + resultC);
    }).catch(function(error){
        return Promise.reject(error);
    });
}
  • Globale Variablen sind schlecht, daher verwendet diese Lösung eine Variable mit lokalem Gültigkeitsbereich, die keinen Schaden verursacht. Es ist nur innerhalb der Funktion zugänglich.
  • Der veränderbare Zustand ist hässlich, aber dies mutiert den Zustand nicht auf hässliche Weise. Der hässliche veränderbare Zustand bezieht sich traditionell auf das Ändern des Zustands von Funktionsargumenten oder globalen Variablen, aber dieser Ansatz ändert einfach den Zustand einer Variablen mit lokalem Gültigkeitsbereich, die nur zum Zweck der Aggregation von Versprechungsergebnissen existiert ... eine Variable, die bei einem einfachen Tod stirbt sobald das Versprechen gelöst ist.
  • Zwischenversprechen werden nicht daran gehindert, auf den Status des Ergebnisobjekts zuzugreifen. Dies führt jedoch nicht zu einem beängstigenden Szenario, in dem eines der Versprechen in der Kette Ihre Ergebnisse verfälscht und sabotiert. Die Verantwortung für die Festlegung der Werte in jedem Schritt des Versprechens ist auf diese Funktion beschränkt, und das Gesamtergebnis ist entweder richtig oder falsch. Es handelt sich nicht um einen Fehler, der Jahre später in der Produktion auftritt (es sei denn, Sie beabsichtigen dies) !)
  • Dies führt kein Race-Bedingungsszenario ein, das sich aus einem parallelen Aufruf ergeben würde, da für jeden Aufruf der Funktion getExample eine neue Instanz der Ergebnisvariablen erstellt wird.

1
Vermeiden Sie zumindest das PromiseKonstruktor-Antimuster !
Bergi

Danke @Bergi, ich habe nicht einmal gemerkt, dass das ein Anti-Muster ist, bis du es erwähnt hast!
Jay

Dies ist eine gute Problemumgehung, um Versprechen-bezogene Fehler zu minimieren. Ich habe ES5 verwendet und wollte keine weitere Bibliothek hinzufügen, um mit Versprechen zu arbeiten.
Nilakantha Singh Deo

8

Knoten 7.4 unterstützt jetzt asynchrone / wartende Anrufe mit dem Harmony-Flag.

Versuche dies:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

und führen Sie die Datei aus mit:

node --harmony-async-await getExample.js

So einfach wie möglich!


8

In diesen Tagen habe ich auch einige Fragen wie Sie getroffen. Endlich finde ich eine gute Lösung mit der Frage, es ist einfach und gut zu lesen. Ich hoffe das kann dir helfen.

Nach How-to-Chain-Javascript-Versprechen

ok, schauen wir uns den Code an:

const firstPromise = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(secondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });

4
Dies beantwortet nicht wirklich die Frage, wie auf frühere Ergebnisse in der Kette zugegriffen werden kann.
Bergi

2
Jedes Versprechen kann den vorherigen Wert erhalten. Was bedeutet es für Sie?
Yzfdjzwl

1
Schauen Sie sich den Code in der Frage an. Ziel ist es nicht, das Ergebnis des angeforderten Versprechens .thenzu erhalten, sondern das Ergebnis davor. ZB thirdPromiseZugriff auf das Ergebnis von firstPromise.
Bergi

6

Eine andere Antwort mit babel-nodeVersion <6

Verwenden von async - await

npm install -g babel@5.6.14

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

Dann babel-node example.jsrenn und voila!


1
Ja, gleich nachdem ich meine gepostet habe. Trotzdem werde ich es verlassen, weil es erklärt, wie man mit ES7 tatsächlich einsatzbereit ist, anstatt nur zu sagen, dass ES7 eines Tages verfügbar sein wird.
Anthony

1
Oh, richtig, ich sollte meine Antwort aktualisieren, um zu sagen, dass die "experimentellen" Plugins für diese bereits hier sind.
Bergi

2

Ich werde dieses Muster nicht in meinem eigenen Code verwenden, da ich kein großer Fan der Verwendung globaler Variablen bin. Zur Not wird es jedoch funktionieren.

Benutzer ist ein versprochenes Mungomodell.

var globalVar = '';

User.findAsync({}).then(function(users){
  globalVar = users;
}).then(function(){
  console.log(globalVar);
});

2
Beachten Sie, dass dieses Muster bereits in der Antwort auf den veränderlichen Kontextstatus detailliert beschrieben ist (und auch, warum es hässlich ist - ich bin auch kein großer Fan)
Bergi

In Ihrem Fall scheint das Muster jedoch nutzlos zu sein. Du brauchst überhaupt keine globalVar, oder User.findAsync({}).then(function(users){ console.log(users); mongoose.connection.close() });?
Bergi

1
Ich brauche es nicht persönlich in meinem eigenen Code, aber der Benutzer muss möglicherweise in der zweiten Funktion mehr Async ausführen und dann mit dem ursprünglichen Promise-Aufruf interagieren. Aber wie bereits erwähnt, werde ich in diesem Fall Generatoren verwenden. :)
Anthony

2

Eine andere Antwort mit sequentiellem Executor nsynjs :

function getExample(){

  var response1 = returnPromise1().data;

  // promise1 is resolved at this point, '.data' has the result from resolve(result)

  var response2 = returnPromise2().data;

  // promise2 is resolved at this point, '.data' has the result from resolve(result)

  console.log(response, response2);

}

nynjs.run(getExample,{},function(){
    console.log('all done');
})

Update: Arbeitsbeispiel hinzugefügt

function synchronousCode() {
     var urls=[
         "https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
     ];
     for(var i=0; i<urls.length; i++) {
         var len=window.fetch(urls[i]).data.text().data.length;
         //             ^                   ^
         //             |                   +- 2-nd promise result
         //             |                      assigned to 'data'
         //             |
         //             +-- 1-st promise result assigned to 'data'
         //
         console.log('URL #'+i+' : '+urls[i]+", length: "+len);
     }
}

nsynjs.run(synchronousCode,{},function(){
    console.log('all done');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>


1

Bei Verwendung von Bluebird können Sie mithilfe der .bindMethode Variablen in der Versprechenskette freigeben:

somethingAsync().bind({})
.spread(function (aValue, bValue) {
    this.aValue = aValue;
    this.bValue = bValue;
    return somethingElseAsync(aValue, bValue);
})
.then(function (cValue) {
    return this.aValue + this.bValue + cValue;
});

Bitte überprüfen Sie diesen Link für weitere Informationen:

http://bluebirdjs.com/docs/api/promise.bind.html


Beachten Sie, dass dieses Muster bereits in der Antwort auf den veränderlichen Kontextstatus
Bergi

1
function getExample() {
    var retA, retB;
    return promiseA(…).then(function(resultA) {
        retA = resultA;
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        //retA is value of promiseA
        return // How do I gain access to resultA here?
    });
}

einfacher Weg: D.


Sie haben diese Antwort bemerkt ?
Bergi

1

Ich denke, Sie können Hash von RSVP verwenden.

So etwas wie unten:

    const mainPromise = () => {
        const promise1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('first promise is completed');
                resolve({data: '123'});
            }, 2000);
        });

        const promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('second promise is completed');
                resolve({data: '456'});
            }, 2000);
        });

        return new RSVP.hash({
              prom1: promise1,
              prom2: promise2
          });

    };


   mainPromise()
    .then(data => {
        console.log(data.prom1);
        console.log(data.prom2);
    });

Ja, das ist die gleiche Lösung wie die Promise.allLösung , nur mit einem Objekt anstelle eines Arrays.
Bergi

0

Lösung:

Sie können Zwischenwerte in jeder späteren Funktion 'then' explizit in den Gültigkeitsbereich einfügen, indem Sie 'bind' verwenden. Es ist eine nette Lösung, bei der die Funktionsweise von Promises nicht geändert werden muss und nur ein oder zwei Codezeilen erforderlich sind, um die Werte weiterzugeben, genau wie Fehler bereits weitergegeben werden.

Hier ist ein vollständiges Beispiel:

// Get info asynchronously from a server
function pGetServerInfo()
    {
    // then value: "server info"
    } // pGetServerInfo

// Write into a file asynchronously
function pWriteFile(path,string)
    {
    // no then value
    } // pWriteFile

// The heart of the solution: Write formatted info into a log file asynchronously,
// using the pGetServerInfo and pWriteFile operations
function pLogInfo(localInfo)
    {
    var scope={localInfo:localInfo}; // Create an explicit scope object
    var thenFunc=p2.bind(scope); // Create a temporary function with this scope
    return (pGetServerInfo().then(thenFunc)); // Do the next 'then' in the chain
    } // pLogInfo

// Scope of this 'then' function is {localInfo:localInfo}
function p2(serverInfo)
    {
    // Do the final 'then' in the chain: Writes "local info, server info"
    return pWriteFile('log',this.localInfo+','+serverInfo);
    } // p2

Diese Lösung kann wie folgt aufgerufen werden:

pLogInfo("local info").then().catch(err);

(Hinweis: Eine komplexere und vollständigere Version dieser Lösung wurde getestet, nicht jedoch diese Beispielversion, sodass möglicherweise ein Fehler vorliegt.)


Dies scheint das gleiche Muster zu sein wie in der Antwort auf Verschachtelungen (und)
Bergi

Es sieht ähnlich aus. Seitdem habe ich erfahren, dass die neue Async / Await-Syntax das automatische Binden von Argumenten umfasst, sodass alle Argumente allen asynchronen Funktionen zur Verfügung stehen. Ich gebe Versprechen auf.
David Spector

async/ awaitBedeutet noch Versprechungen verwenden. Was Sie möglicherweise aufgeben, sind thenAnrufe mit Rückrufen.
Bergi

-1

Was ich über Versprechen lerne, ist, sie nur zu verwenden, wenn Rückgabewerte möglichst nicht auf sie verweisen . Die asynchrone / warten-Syntax ist dafür besonders praktisch. Heute unterstützen es alle neuesten Browser und Knoten: https://caniuse.com/#feat=async-functions , ist ein einfaches Verhalten und der Code ist wie das Lesen von synchronem Code, vergessen Sie Rückrufe ...

In Fällen, in denen ich auf ein Versprechen verweisen muss, erfolgt die Erstellung und Lösung an unabhängigen / nicht verwandten Orten. Stattdessen bevorzuge ich eine künstliche Assoziation und wahrscheinlich einen Ereignis-Listener, um das "entfernte" Versprechen zu lösen. Ich ziehe es vor, das Versprechen als verzögert darzustellen, was der folgende Code in gültigem es5 implementiert

/**
 * Promise like object that allows to resolve it promise from outside code. Example:
 *
```
class Api {
  fooReady = new Deferred<Data>()
  private knower() {
    inOtherMoment(data=>{
      this.fooReady.resolve(data)
    })
  }
}
```
 */
var Deferred = /** @class */ (function () {
  function Deferred(callback) {
    var instance = this;
    this.resolve = null;
    this.reject = null;
    this.status = 'pending';
    this.promise = new Promise(function (resolve, reject) {
      instance.resolve = function () { this.status = 'resolved'; resolve.apply(this, arguments); };
      instance.reject = function () { this.status = 'rejected'; reject.apply(this, arguments); };
    });
    if (typeof callback === 'function') {
      callback.call(this, this.resolve, this.reject);
    }
  }
  Deferred.prototype.then = function (resolve) {
    return this.promise.then(resolve);
  };
  Deferred.prototype.catch = function (r) {
    return this.promise.catch(r);
  };
  return Deferred;
}());

transpiliert von einem Typoskript-Projekt von mir:

https://github.com/cancerberoSgx/misc-utils-of-mine/blob/2927c2477839f7b36247d054e7e50abe8a41358b/misc-utils-of-mine-generic/src/promise.ts#L31

Für komplexere Fälle verwende ich oft diese kleinen Versprechungsdienstprogramme ohne Abhängigkeiten, die getestet und getippt wurden. p-map war schon mehrmals nützlich. Ich denke, er hat die meisten Anwendungsfälle abgedeckt:

https://github.com/sindresorhus?utf8=%E2%9C%93&tab=repositories&q=promise&type=source&language=


Klingt so, als würden Sie entweder einen veränderlichen Kontextstatus oder eine synchrone Überprüfung vorschlagen ?
Bergi

@bergi Zum ersten Mal gehe ich diese Namen an. Ich kenne diese Art von selbstbewussten Versprechungen unter dem Namen Deferred - Übrigens ist die Implementierung nur ein Versprechen mit Entschlossenheit. Ich brauche dieses Muster oft in Fällen, in denen die Verantwortung für die Erstellung und Lösung von Versprechen unabhängig ist, sodass es nicht erforderlich ist, sie nur zur Lösung eines Versprechens in Beziehung zu setzen. Ich habe mich angepasst, aber nicht für Ihr Beispiel, und eine Klasse verwendet, aber vielleicht gleichwertig.
Cancerbero
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.