Wie kann ich warten, bis ein JavaScript-Versprechen aufgelöst ist, bevor ich die Funktion wieder aufnehme?

107

Ich mache einige Unit-Tests. Das Testframework lädt eine Seite in einen iFrame und führt dann Zusicherungen für diese Seite aus. Bevor jeder Test beginnt, erstelle ich ein Ereignis, Promisedas das aufzurufende onloadEreignis resolve()des iFrames festlegt, das des iFrames festlegt srcund das Versprechen zurückgibt.

Ich kann also einfach anrufen loadUrl(url).then(myFunc)und es wird warten, bis die Seite geladen ist, bevor etwas ausgeführt myFuncwird.

Ich verwende diese Art von Muster überall in meinen Tests (nicht nur zum Laden von URLs), hauptsächlich, um Änderungen am DOM zuzulassen (z. B. das Klicken auf eine Schaltfläche nachahmen und darauf warten, dass Divs ausgeblendet und angezeigt werden).

Der Nachteil dieses Designs ist, dass ich ständig anonyme Funktionen mit ein paar Codezeilen schreibe. Während ich eine Problemumgehung (QUnit assert.async()) habe, wird die Testfunktion, die die Versprechen definiert, abgeschlossen, bevor das Versprechen ausgeführt wird.

Ich frage mich, ob es eine Möglichkeit gibt, einen Wert von a Promiseabzurufen oder zu warten (block / sleep), bis er aufgelöst ist, ähnlich wie bei .NET IAsyncResult.WaitHandle.WaitOne(). Ich weiß, dass JavaScript Single-Threaded ist, aber ich hoffe, dass dies nicht bedeutet, dass eine Funktion nicht nachgeben kann.

Gibt es im Wesentlichen eine Möglichkeit, die folgenden Ergebnisse in der richtigen Reihenfolge auszuspucken?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

dx_over_dt
quelle
Wenn Sie die Append-Aufrufe in eine wiederverwendbare Funktion versetzen, können Sie dann () nach Bedarf DRY. Sie können auch Mehrzweck-Handler erstellen, die von diesen gesteuert werden , um dann () -Aufrufe zu füttern, wie .then(fnAppend.bind(myDiv))z. B. , die Anonen erheblich reduzieren können.
Dandavis
Womit testest du? Wenn es sich um einen modernen Browser handelt oder Sie ein Tool wie BabelJS verwenden können, um Ihren Code zu transpilieren, ist dies sicherlich möglich.
Benjamin Gruenbaum

Antworten:

78

Ich frage mich, ob es eine Möglichkeit gibt, einen Wert aus einem Versprechen abzurufen oder zu warten (blockieren / schlafen), bis er aufgelöst ist, ähnlich wie bei IAsyncResult.WaitHandle.WaitOne () von .NET. Ich weiß, dass JavaScript Single-Threaded ist, aber ich hoffe, dass dies nicht bedeutet, dass eine Funktion nicht nachgeben kann.

Die aktuelle Generation von Javascript in Browsern verfügt nicht über ein wait()oder sleep(), mit dem andere Dinge ausgeführt werden können. Sie können also einfach nicht tun, was Sie verlangen. Stattdessen hat es asynchrone Operationen, die ihre Sache erledigen und Sie dann anrufen, wenn sie fertig sind (wie Sie es für Versprechen verwendet haben).

Ein Teil davon ist auf die Single-Threadedness von Javascript zurückzuführen. Wenn sich der einzelne Thread dreht, kann kein anderes Javascript ausgeführt werden, bis dieser sich drehende Thread fertig ist. ES6 führt yieldund generiert Generatoren, die solche kooperativen Tricks ermöglichen, aber wir sind weit davon entfernt, diese in einer Vielzahl installierter Browser zu verwenden (sie können in einigen serverseitigen Entwicklungen verwendet werden, in denen Sie die JS-Engine steuern das wird verwendet).


Eine sorgfältige Verwaltung von versprechungsbasiertem Code kann die Ausführungsreihenfolge für viele asynchrone Vorgänge steuern.

Ich bin mir nicht sicher, ob ich genau verstehe, welche Reihenfolge Sie in Ihrem Code erreichen möchten, aber Sie könnten so etwas mit Ihrer vorhandenen kickOff()Funktion tun und dann .then()nach dem Aufrufen einen Handler daran anhängen :

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");

    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

Dadurch wird die Ausgabe in einer garantierten Reihenfolge zurückgegeben - wie folgt:

start
middle
end

Update im Jahr 2018 (drei Jahre nach dieser Antwort):

Wenn Sie Ihren Code entweder transpilieren oder in einer Umgebung ausführen, die ES7-Funktionen wie asyncund unterstützt await, können Sie awaitIhren Code jetzt "erscheinen" lassen, um auf das Ergebnis eines Versprechens zu warten. Es entwickelt sich immer noch mit Versprechen. Es blockiert immer noch nicht das gesamte Javascript, aber es ermöglicht Ihnen, sequentielle Operationen in einer freundlicheren Syntax zu schreiben.

Anstelle der ES6-Methode:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

Du kannst das:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});
jfriend00
quelle
3
Richtig, mit then()ist, was ich getan habe. Ich mag es einfach nicht, die function() { ... }ganze Zeit schreiben zu müssen . Es überfrachtet den Code.
dx_over_dt
3
@dfoverdx - Die asynchrone Codierung in Javascript beinhaltet immer Rückrufe, sodass Sie immer eine Funktion definieren müssen (anonym oder benannt). Derzeit führt kein Weg daran vorbei.
jfriend00
2
Sie können jedoch die ES6-Pfeilnotation verwenden, um sie knapper zu machen. (foo) => { ... }stattfunction(foo) { ... }
Rob H
1
Wenn Sie häufig zu @ RobH-Kommentaren hinzufügen, können Sie auch schreiben foo => single.expression(here), um geschweifte und runde Klammern zu entfernen .
Begie
3
@dfoverdx - Drei Jahre nach meiner Antwort habe ich es mit ES7 asyncund awaitSyntax als Option aktualisiert, die jetzt in node.js und in modernen Browsern oder über Transpiler verfügbar ist.
jfriend00
15

Wenn ES2016 verwenden , können Sie verwenden , asyncund awaitund so etwas wie:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

Wenn Sie ES2015 verwenden, können Sie Generatoren verwenden . Wenn Sie nicht wie die Syntax tun abstrakt Sie können es mit entfernt eine asyncNutzenfunktion wie hier erläutert .

Wenn Sie ES5 verwenden, möchten Sie wahrscheinlich eine Bibliothek wie Bluebird , die Ihnen mehr Kontrolle gibt.

Wenn Ihre Laufzeit ES2015 unterstützt, kann die bereits ausgeführte Ausführungsreihenfolge mithilfe von Fetch Injection parallel beibehalten werden .

Josh Habdas
quelle
1
Ihr Generatorbeispiel funktioniert nicht, es fehlt ein Läufer. Bitte empfehlen Sie keine Generatoren mehr, wenn Sie nur ES8 async/ transpilieren können await.
Bergi
3
Vielen Dank für Ihr Feedback. Ich habe das Generator-Beispiel entfernt und stattdessen ein umfassenderes Generator-Tutorial mit zugehöriger OSS-Bibliothek verlinkt.
Josh Habdas
9
Dies schließt einfach das Versprechen ein. Die asynchrone Funktion wartet beim Ausführen nicht auf etwas, sondern gibt nur ein Versprechen zurück. async/ awaitist nur eine andere Syntax für Promise.then.
Rustyx
Sie haben absolut Recht async, warten Sie nicht ... es sei denn, es gibt Verwendung await. Das ES2016 / ES7-Beispiel kann immer noch nicht SO ausgeführt werden. Hier ist ein Code, den ich vor drei Jahren geschrieben habe, um das Verhalten zu demonstrieren.
Josh Habdas
7

Eine andere Option ist die Verwendung von Promise.all, um auf die Auflösung einer Reihe von Versprechungen zu warten. Der folgende Code zeigt, wie Sie warten müssen, bis alle Versprechen gelöst sind, und dann mit den Ergebnissen umgehen, sobald sie alle fertig sind (da dies das Ziel der Frage zu sein schien). Start könnte jedoch offensichtlich ausgegeben werden, bevor die Mitte aufgelöst wurde, indem dieser Code hinzugefügt wird, bevor die Auflösung aufgerufen wird.

function kickOff() {
  let start = new Promise((resolve, reject) => resolve("start"))
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => resolve(" middle"), 1000)
  })
  let end = new Promise((resolve, reject) => resolve(" end"))

  Promise.all([start, middle, end]).then(results => {
    results.forEach(result => $("#output").append(result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

Stan Kurdziel
quelle
1

Sie können es manuell tun. (Ich weiß, dass das keine gute Lösung ist, aber ..) benutze whileloop, bis der resultkeinen Wert mehr hat

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
quelle
Dies würde die gesamte CPU verbrauchen, während Sie warten.
Lukasz Guminski