wake-up-neo.com

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

Ich habe meinen Code in Versprechen umstrukturiert und eine wunderbare lange flache Versprechen-Kette aufgebaut, die aus mehreren .then() besteht. Rückrufe. Am Ende möchte ich einen zusammengesetzten Wert zurückgeben und muss auf mehrere Zwischenversprechungsergebnisse zugreifen . Die Auflösungswerte aus der Mitte der Sequenz sind im letzten Rückruf jedoch nicht gültig. 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?
    });
}
590
Bergi

ECMAScript Harmony

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

ECMAScript 8

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

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 ausgeliefert, mit denen die Ausführung an beliebig platzierten yield Schlüsselwörtern aufgeteilt werden kann. Diese Slices können unabhängig voneinander oder sogar asynchron nacheinander ausgeführt werden - und genau das tun wir, wenn wir auf eine versprochene 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 vielversprechende Bibliotheken haben Hilfsfunktionen ( Q , Bluebird , wann ,…) die diese asynchrone schrittweise Ausführung für Sie tun, wenn Sie ihnen eine Generatorfunktion geben, die Versprechen gibt.

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, und auch einige Browser (oder ihre Entwicklungseditionen) haben die Generatorsyntax relativ früh unterstützt.

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 von den aktuellen Tools unterstützt, siehe zum Beispiel die Dokumentation von Babel zu Generatoren und Async-Funktionen .

Und dann gibt es noch viele andere in JS-Sprachen kompilieren , die der Erleichterung der asynchronen Programmierung gewidmet sind. Sie verwenden normalerweise eine ähnliche Syntax wie await (z. B. Iced CoffeeScript ), aber es gibt auch andere, die eine Haskell-ähnliche do- -Notation aufweisen (z. B. LatteJs , monadisch , PureScript oder LispyScript ).

217
Bergi

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, hängen Sie mehrere Rückrufe an dasselbe Versprechen an - wo immer Sie den Ergebniswert benötigen. Vergiss nicht, ein Versprechen repräsentiert nur einen zukünftigen Wert ! Verwenden Sie neben der Ableitung eines Versprechens von dem anderen in einer linearen Kette die Versprechungskombinatoren, die Sie von Ihrer Bibliothek erhalten, um den Ergebniswert zu erstellen.

Dies führt zu einem sehr einfachen Kontrollfluss, einer klaren Zusammenstellung 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 Parameterdestrukturierung im Callback nach Promise.all, die erst mit ES6 verfügbar wurde, wurde in ES5 der then -Aufruf durch eine raffinierte Hilfsmethode ersetzt, die von vielen vielversprechenden Bibliotheken bereitgestellt wurde ( Q , Bluebird , wenn ,…): .spread(function(resultA, resultB) { ….

Bluebird bietet auch eine dedizierte join -Funktion , um diese Promise.all + spread -Kombination durch ein einfacheres (und effizienteres) Konstrukt zu ersetzen:

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

Synchrone Inspektion

Zuweisen von Versprechungen für später benötigte Werte zu Variablen und anschließendes Abrufen ihres Werts durch synchrone Überprüfung. In diesem Beispiel wird die .value() -Methode von bluebird verwendet, aber viele Bibliotheken bieten ähnliche Methoden 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();
    });
}
97
Esailija

Nesting (und) Verschlüsse

Die Verwendung von Closures zur Beibehaltung des Gültigkeitsbereichs von Variablen (in unserem Fall der Parameter der Success Callback-Funktion) ist die natürliche JavaScript-Lösung. Mit Versprechungen können wir willkürlich Rückrufe verschachteln und abflachen.then() - 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 Einkerbungspyramide. Wenn die Einrückung zu groß wird, können Sie immer noch die alten Werkzeuge anwenden, um der Pyramide des Schicksals entgegenzuwirken : Modularisieren, zusätzliche benannte Funktionen verwenden und das Versprechen reduzieren verketten, sobald Sie keine Variable mehr benötigen.
Theoretisch können Sie immer mehr als zwei Verschachtelungsebenen vermeiden (indem Sie alle Verschlüsse explizit angeben). In der Praxis werden so viele verwendet, wie angemessen 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 für diese Art von Teilanwendung auch Hilfsfunktionen verwenden, wie _.partial from Underscore / lodash oder die native .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
}
51
Bergi

Explizites Pass-Through

Ähnlich wie beim Verschachteln der Rückrufe stützt sich diese Technik auf Verschlüsse. Die Kette bleibt jedoch flach - anstatt nur das neueste Ergebnis zu übergeben, wird bei jedem Schritt ein Statusobjekt übergeben. Diese Statusobjekte akkumulieren die Ergebnisse der vorherigen Aktionen und geben alle Werte an, die später erneut benötigt werden, sowie das Ergebnis der aktuellen Aufgabe.

_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 der kleine Pfeil _b => [resultA, b]_ die Funktion, die resultA schließt und ein Array beider Ergebnisse an den nächsten Schritt übergibt. Wobei die Syntax der Parameter-Destrukturierung verwendet wird, um sie wieder in einzelne Variablen aufzuteilen.

Bevor die Destrukturierung mit ES6 verfügbar wurde, wurde von vielen vielversprechenden Bibliotheken eine nützliche Hilfsmethode namens .spread() bereitgestellt ( Q , Bluebird , when ,…). Eine Funktion mit mehreren Parametern - einer für jedes Array-Element - wird als .spread(function(resultA, resultB) { … verwendet.

Natürlich kann dieser hier erforderliche 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.all_ verwenden, um das Versprechen für das Array zu 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 verwenden möglicherweise nicht nur Arrays, sondern auch beliebig komplexe Objekte. Zum Beispiel mit _.extend oder Object.assign in 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 Statusobjekte die Übersichtlichkeit verbessern können, wird es für eine lange Kette mühsam. Besonders wenn Sie den Staat nur sporadisch brauchen, müssen Sie ihn noch jeden Schritt durchlaufen. Mit dieser festen Schnittstelle sind die einzelnen Rückrufe in der Kette ziemlich eng gekoppelt und können nicht geändert werden. Es erschwert das Ausklammern einzelner Schritte, und Rückrufe können nicht direkt von anderen Modulen geliefert werden. Sie müssen immer in einen Code eingebettet sein, der den Status berücksichtigt. Abstrakte Hilfsfunktionen wie die oben genannten können den Schmerz etwas lindern, sind aber immer vorhanden.

49
Bergi

Veränderlicher kontextueller Zustand

Die triviale (aber unelegante und ziemlich fehleranfällige) Lösung besteht darin, einfach Variablen mit höherem Gültigkeitsbereich (auf die alle Rückrufe in der Kette zugreifen können) zu verwenden und Ergebniswerte zu schreiben, wenn Sie diese 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 man auch ein (anfangs leeres) Objekt verwenden, auf dem die Ergebnisse als dynamisch erzeugte Eigenschaften gespeichert werden.

Diese Lösung hat mehrere Nachteile:

  • Veränderlicher 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 Gültigkeitsbereich der Variablen verhindert nicht, dass auf sie zugegriffen werden kann, bevor sie initialisiert werden. Dies ist besonders wahrscheinlich für komplexe Versprechungskonstruktionen (Schleifen, Verzweigungen, Ausnahmen), bei denen Rennbedingungen auftreten können. Wird der Status explizit übergeben, erzwingt ein deklaratives Design , das Anreize verspricht, 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 würde.

Die Bluebird-Bibliothek empfiehlt die Verwendung eines weitergegebenen Objekts mit ihrer bind() -Methode , um ein Kontextobjekt einer Versprechen-Kette zuzuweisen. Der Zugriff auf die einzelnen Rückruffunktionen erfolgt über das ansonsten nicht verwendbare this keyword . Während Objekteigenschaften anfälliger für unerkannte Tippfehler sind als Variablen, 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 (wenn auch etwas ausführlicher und nicht in einem Ausdruck verwendbar):

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));
}
32
Bergi

Ein weniger harter Dreh zu "Mutable contextual state"

Die Verwendung eines Objekts mit lokalem Geltungsbereich zum Sammeln der Zwischenergebnisse in einer Versprechen-Kette ist eine vernünftige Herangehensweise an 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. Sie ist nur innerhalb der Funktion zugänglich.
  • Veränderlicher Zustand ist hässlich, aber dies verändert 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 lokal begrenzten Variablen, die nur zum Zwecke der Aggregation von Versprechungsergebnissen existiert ... eine Variable, die einen einfachen Tod sterben wird 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 unangemessen wird und Ihre Ergebnisse sabotiert. Die Verantwortung für die Einstellung der Werte in jedem Schritt des Versprechens ist auf diese Funktion beschränkt und das Gesamtergebnis wird entweder richtig oder falsch sein. Es wird kein Fehler sein, der Jahre später in der Produktion auftaucht (es sei denn, Sie beabsichtigen dies) !)
  • Dies führt kein Race-Condition-Szenario 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.
11
Jay

Knoten 7.4 unterstützt jetzt Async/Warten-Anrufe mit dem Harmony-Flag.

Versuche dies:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

und starte die Datei mit:

node --harmony-async-await getExample.js

So einfach kann es sein!

7
Antoine

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

Eine andere Antwort mit babel-node Version <6

async - await verwenden

npm install -g [email protected]

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

Dann starte babel-node example.js und voila!

5
Antoine

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>
2
amaksr

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

Benutzer ist ein vielversprechendes Mungomodell.

var globalVar = '';

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

Wenn Sie Bluebird verwenden, können Sie die .bind -Methode verwenden, um Variablen in der Versprechen-Kette zu teilen:

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

1
alphakevin
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

1
Minh Giang

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

Sowas 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);
    });
1
Vishu

Lösung:

Sie können Zwischenwerte in den Gültigkeitsbereich einer späteren 'then'-Funktion explizit einfügen, indem Sie' bind 'verwenden. Es ist eine gute Lösung, bei der die Funktionsweise von Promises nicht geändert werden muss. Es sind nur ein oder zwei Zeilen Code erforderlich, 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: Es wurde eine komplexere und vollständigere Version dieser Lösung getestet, jedoch nicht diese Beispielversion, sodass ein Fehler auftreten kann.)

0
David Spector