Visualisierung von Versprechen und Async / Await



Guten Tag, Freunde!

Ich präsentiere Ihnen die Übersetzung des Artikels "JavaScript Visualized: Promises & Async / Await" von Lydia Hallie.

Sind Sie auf JavaScript-Code gestoßen, der ... nicht wie erwartet funktioniert? Wenn Funktionen in einer beliebigen, unvorhersehbaren Reihenfolge oder mit Verzögerung ausgeführt werden. Eine der Hauptaufgaben von Versprechungen ist die Rationalisierung der Ausführung von Funktionen.

Meine unersättliche Neugier und schlaflosen Nächte haben sich voll ausgezahlt - dank ihnen habe ich mehrere Animationen erstellt. Es ist Zeit, über Versprechen zu sprechen: wie sie funktionieren, warum sie verwendet werden sollten und wie es gemacht wird.

Einführung


Beim Schreiben von JS-Code müssen wir uns häufig mit Aufgaben befassen, die von anderen Aufgaben abhängen. Angenommen, wir möchten ein Bild abrufen, komprimieren, einen Filter darauf anwenden und es speichern.

Zunächst müssen wir uns ein Bild machen. Verwenden Sie dazu die Funktion getImage. Nach dem Laden des Bildes übergeben wir es an die Funktion resizeImage. Nach dem Komprimieren des Bildes wenden wir mithilfe der Funktion einen Filter darauf an applyFilter. Nach der Komprimierung und Anwendung des Filters speichern wir das Bild und informieren den Benutzer über den Erfolg.

Als Ergebnis erhalten wir Folgendes:



Hmm ... Hast du etwas bemerkt? Trotz der Tatsache, dass alles funktioniert, sieht es nicht gut aus. Wir erhalten viele verschachtelte Rückruffunktionen, die von früheren Rückrufen abhängen. Dies wird als Callback-Hölle bezeichnet und macht es sehr schwierig, Code zu lesen und zu warten.

Zum Glück haben wir heute Versprechen.



Versprechenssyntax


Versprechen wurden in ES6 eingeführt. In vielen Handbüchern können Sie Folgendes lesen:

Ein Versprechen (Versprechen) ist ein Wert, der in Zukunft erfüllt oder abgelehnt wird.

Ja ... so lala Erklärung. Zu einer Zeit hielt ich Versprechen für etwas Seltsames, Unbestimmtes, eine Art Magie. Was sind sie wirklich?

Wir können ein Versprechen mit einem Konstruktor erstellen Promise, der eine Rückruffunktion als Argument verwendet. Cool, lass uns versuchen:



Warte, was kommt hier zurück?

PromiseIst ein Objekt, das status ( [[PromiseStatus]]) und value ( [[PromiseValue]]) enthält. In dem obigen Beispiel wird der Wert [[PromiseStatus]]ist pending, und der Wert der Verheißung ist undefined.

Keine Sorge, Sie müssen nicht mit diesem Objekt interagieren, Sie können nicht einmal auf die Eigenschaften [[PromiseStatus]]und zugreifen [[PromiseValue]]. Diese Eigenschaften sind jedoch sehr wichtig, wenn Sie mit Versprechungen arbeiten.

PromiseStatus oder der Status eines Versprechens kann einen von drei Werten annehmen:

  • fulfilled: resolved (). ,
  • rejected: rejected (). -
  • pending: , pending (, )

Klingt gut, aber wann erhält ein Versprechen den angegebenen Status? Und warum ist Status wichtig?

Im obigen Beispiel übergeben wir eine Promiseeinfache Rückruffunktion an den Konstruktor () => {}. Diese Funktion akzeptiert tatsächlich zwei Argumente. Der Wert des ersten Arguments, normalerweise aufgerufen resolveoder res, ist die Methode, die aufgerufen wird, wenn das Versprechen ausgeführt wird. Der Wert des zweiten Arguments, normalerweise aufgerufen rejectoder rej, ist die Methode, die aufgerufen wird, wenn das Versprechen abgelehnt wird, wenn etwas schief gelaufen ist.



Mal sehen, was beim Aufrufen der Methoden an die Konsole ausgegeben wird resolveund reject:



Cool! Jetzt wissen wir, wie wir Status pendingund Bedeutung loswerden können undefined. Der Status der Versprechen beim Aufruf der Methode resolveist fulfilled, wenn reject-rejected.

[[PromiseValue]]oder der Wert des Versprechens ist der Wert, den wir an die Methoden resolveoder rejectals Argument übergeben.

Unterhaltsame Tatsache: Jake Archibald wies nach dem Lesen dieses Artikels auf einen Fehler in Chrome hin, der stattdessen fulfilledzurückkehrte resolved.



Ok, jetzt wissen wir, wie man mit dem Objekt arbeitet Promise. Aber wofür wird es verwendet?

In der Einleitung habe ich ein Beispiel gegeben, in dem wir ein Bild erhalten, es komprimieren, einen Filter darauf anwenden und es speichern. Dann endete alles mit der Hölle des Rückrufs.

Glücklicherweise helfen Versprechen, damit umzugehen. Wir schreiben den Code so um, dass jede Funktion ein Versprechen zurückgibt.

Wenn das Bild geladen wurde, halten wir ein Versprechen. Andernfalls lehnen Sie das Versprechen ab, wenn ein Fehler auftritt:



Mal sehen, was passiert, wenn dieser Code im Terminal ausgeführt wird:



Cool! Promis kehrt erwartungsgemäß mit analysierten ("analysierten") Daten zurück.

Aber ... was kommt als nächstes? Wir interessieren uns nicht für das Thema Promis, wir interessieren uns für dessen Daten. Es gibt 3 integrierte Methoden, um einen Promise-Wert zu erhalten:

  • .then(): nach Erfüllung eines Versprechens angerufen
  • .catch(): nach der Ablehnung des Versprechens angerufen
  • .finally(): immer aufgerufen, sowohl nach Ausführung als auch nach Ablehnung eines Versprechens



Die Methode .thenübernimmt den an die Methode übergebenen Wert resolve:



Die Methode .catchübernimmt den an die Methode übergebenen Wert reject:



Schließlich haben wir den gewünschten Wert erhalten. Mit diesem Wert können wir alles machen.

Wenn wir von der Erfüllung oder Ablehnung eines Versprechens überzeugt sind, können Sie Promise.resolveentweder Promise.rejectmit dem entsprechenden Wert schreiben .



Dies ist die Syntax, die in den folgenden Beispielen verwendet wird.



Das Ergebnis .thenist der Wert des Versprechens (d. H. Diese Methode gibt auch das Versprechen zurück). Dies bedeutet, dass wir so viel .thenwie nötig verwenden können: Das Ergebnis des vorherigen .thenwird als Argument an das nächste übergeben .then.



In können getImagewir mehrere verwenden .then, um das verarbeitete Bild zur nächsten Funktion zu übertragen.



Diese Syntax sieht viel besser aus als die Leiter der verschachtelten Rückruffunktionen.



Mikrotasks und (Makro-) Tasks


Nun wissen wir, wie man Versprechen schafft und Werte daraus extrahiert. Fügen Sie unserem Skript Code hinzu und führen Sie ihn erneut aus:



Zuerst wird er in der Konsole angezeigt Start!. Dies ist normal, da wir die erste Codezeile haben console.log('Start!'). Der zweite Wert, der auf der Konsole angezeigt wird End!, ist nicht der Wert des abgeschlossenen Versprechens. Der Wert des Versprechens wird zuletzt angezeigt. Warum ist das geschehen?

Hier sehen wir die Kraft der Versprechen. Obwohl JS Single-Threaded ist, können wir den Code asynchron machen mit Promise.

Wo sonst könnten wir asynchrones Verhalten beobachten? Einige im Browser integrierte Methoden, z. B. setTimeout, können Asynchronität simulieren.

Recht In der Ereignisschleife (Ereignisschleife) gibt es zwei Arten von Warteschlangen: die Warteschlange von (Makro-) Aufgaben oder nur Aufgaben ((Makro-) Aufgabenwarteschlange, Aufgabenwarteschlange) und die Warteschlange von Mikrotasks oder nur Mikrotasks (Mikrotask-Warteschlange, Mikrotasks).

Was gilt für jeden von ihnen? Kurz gesagt:

  • Makroaufgaben: setTimeout, setInterval, setImmediate
  • Mikrotasks: process.nextTick, Promise callback, queueMicrotask

Wir sehen Promisein der Liste der Mikrotasks. Wenn die PromiseMethode ausgeführt und aufgerufen wird then(), catch()oder finally()wird die Rückruffunktion mit der Methode zur Mikrotask-Warteschlange hinzugefügt. Dies bedeutet, dass der Rückruf mit der Methode nicht sofort ausgeführt wird, wodurch JS-Code asynchron wird.

Wann ist die Methode then(), catch()oder finally()wird sie durchgeführt? Aufgaben in der Ereignisschleife haben folgende Priorität:

  1. Zunächst werden die Funktionen im Aufrufstapel ausgeführt. Die von diesen Funktionen zurückgegebenen Werte werden vom Stapel entfernt.
  2. Nachdem der Stapel freigegeben wurde, werden Mikrotasks nacheinander platziert und ausgeführt (Mikrotasks können andere Mikrotasks zurückgeben, wodurch ein endloser Zyklus von Mikrotasks entsteht).
  3. Nachdem der Stapel und die Mikrotask-Warteschlange freigegeben wurden, sucht die Ereignisschleife nach Makros. Makroaufgaben werden auf den Stapel verschoben, ausgeführt und gelöscht.



Betrachten Sie ein Beispiel:

  • Task1: Eine Funktion, die dem Stapel sofort hinzugefügt wird, z. B. durch einen Aufruf von Code.
  • Task2, Task3, Task4: Mikrozadachi Beispiel thenPromi oder Aufgabe hinzugefügt über queueMicrotask.
  • Task5, Task6: Makroaufgaben zum Beispiel setTimeoutodersetImmediate



Zuerst Task1gibt einen Wert und wird aus dem Stapel entfernt. Dann sucht die Engine in der entsprechenden Warteschlange nach Mikrotasks. Nach dem Hinzufügen und anschließenden Entfernen von Mikrotasks zum Stapel sucht die Engine nach Makro-Tasks, die ebenfalls zum Stack hinzugefügt und nach Rückgabe der Werte von diesem entfernt werden.

Genug Worte. Schreiben wir den Code.



In diesem Code haben wir eine Makroaufgabe setTimeoutund eine Mikroaufgabe .then. Führen Sie den Code aus und sehen Sie, was in der Konsole angezeigt wird.

Hinweis: in dem obigen Beispiel verwende ich Methoden wie console.log, setTimeoutund Promise.resolve. Alle diese Methoden sind intern, sodass sie nicht im Stack-Trace angezeigt werden. Seien Sie nicht überrascht, wenn Sie sie nicht in den Tools zur Fehlerbehebung des Browsers finden.

In der ersten Zeile haben wirconsole.log. Es wird dem Stapel hinzugefügt und in der Konsole angezeigt Start!. Danach wird diese Methode vom Stapel entfernt und die Engine analysiert den Code weiter.



Der Motor erreicht setTimeout, was dem Stapel hinzugefügt wird. Diese Methode ist eine integrierte Browsermethode: Ihre Rückruffunktion ( () => console.log('In timeout')) wird der Web-API hinzugefügt und ist vorhanden, bevor der Timer ausgelöst wird. Trotz der Tatsache, dass der Timer-Zähler 0 ist, wird der Rückruf immer noch zuerst in der WebAPI und dann in der Warteschlange für Makroaufgaben platziert: setTimeout- Dies ist eine Makroaufgabe.



Als nächstes erreicht der Motor die Methode Promise.resolve(). Diese Methode wird dem Stapel hinzugefügt und dann mit einem Wert ausgeführt Promise. Sein Rückruf thenwird in die Mikrotask-Warteschlange gestellt.



Schließlich erreicht der Motor die zweite Methode console.log(). Es wird sofort auf den Stapel geschoben und an die Konsole ausgegebenEnd!wird die Methode vom Stapel entfernt und die Engine fährt fort.



Die Engine "sieht", dass der Stapel leer ist. Überprüfen der Aufgabenwarteschlange. Es befindet sich dort then. Wird es auf den Stapel geschoben, wird der Wert des Versprechens auf der Konsole angezeigt: in diesem Fall eine Zeichenfolge Promise!.



Die Engine sieht, dass der Stapel leer ist. Er "schaut" in die Warteschlange der Mikrotasks. Sie ist auch leer.

Es ist Zeit, die Makro-Task-Warteschlange zu überprüfen: Sie ist da setTimeout. Es wird auf den Stapel geschoben und gibt eine Methode zurück console.log(). Eine Zeichenfolge wird an die Konsole ausgegeben 'In timeout!'. setTimeoutwird vom Stapel entfernt.



Erledigt. Jetzt passte alles zusammen, oder?



Async / warten


ES7 führte eine neue Methode zum Arbeiten mit asynchronem Code in JS ein. Mit Hilfe der Schlüsselwörter asyncund awaitwir können eine asynchrone Funktion erstellen , die implizit ein Versprechen zurückgibt. Aber ... wie machen wir das?

Zuvor haben wir erläutert, wie ein Objekt explizit erstellt wird Promise: using new Promise(() => {}), Promise.resolveor Promise.reject.

Stattdessen können wir eine asynchrone Funktion erstellen, die implizit das angegebene Objekt zurückgibt. Dies bedeutet, dass wir nicht mehr manuell erstellen müssen Promise.



Die Tatsache, dass eine asynchrone Funktion implizit ein Versprechen zurückgibt, ist natürlich großartig, aber die Leistungsfähigkeit dieser Funktion wird bei Verwendung eines Schlüsselworts voll zum Ausdruck gebracht await.awaitLässt die asynchrone Funktion warten, bis das Versprechen (sein Wert) abgeschlossen ist. Um den Wert des durchgeführten Versprechens zu erhalten, müssen wir der Variablen den erwarteten (erwarteten) Wert des Versprechens zuweisen.

Es stellt sich heraus, dass wir die Ausführung einer asynchronen Funktion verzögern können? Großartig, aber ... was bedeutet das?

Mal sehen, was passiert, wenn Sie den folgenden Code ausführen:







Zuerst sieht der Motor console.log. Diese Methode wird auf den Stapel geschoben und auf der Konsole angezeigt Before function!.



Dann wird die asynchrone Funktion aufgerufen myFunc(), ihr Code wird ausgeführt. In der ersten Zeile dieses Codes rufen wir die zweite console.logmit einer Zeile auf 'In function!'. Diese Methode wird dem Stapel hinzugefügt, ihr Wert wird auf der Konsole angezeigt und vom Stapel entfernt.



Der Funktionscode wird als nächstes ausgeführt. In der zweiten Zeile haben wir ein Schlüsselwort await.

Das erste, was hier passiert, ist die Ausführung des erwarteten Werts: in diesem Fall die Funktion one. Es wird auf den Stapel geschoben und gibt ein Versprechen zurück. Nachdem das Versprechen erfüllt wurde und die Funktion oneden Wert zurückgegeben hat, sieht der Motor await.

Danach wird die Ausführung der asynchronen Funktion verzögert. Die Ausführung des Funktionskörpers wird angehalten, der verbleibende Code wird als Mikrotask ausgeführt.



Nachdem die Ausführung der asynchronen Funktion verzögert wurde, kehrt die Engine zur Ausführung des Codes in einem globalen Kontext zurück.



Nachdem der gesamte Code in einem globalen Kontext ausgeführt wurde, sucht die Ereignisschleife nach Mikrotasks und erkennt diese myFunc(). myFunc()auf den Stapel geschoben und ausgeführt.

Die Variable reserhält den Wert des ausgeführten Versprechens, das von der Funktion zurückgegeben wird one. Wir rufen console.logmit dem Wert der Variablen auf res: One!in diesem Fall eine Zeichenfolge . One!Es wird in der Konsole angezeigt.

Erledigt. Beachten Sie den Unterschied zwischen der asynchronen Funktion und der thenPromis- Methode ? Stichwortawaitverschiebt die Ausführung einer asynchronen Funktion. Wenn wir verwenden würden then, würde der Promise-Körper weiterlaufen.



Es stellte sich als ziemlich ausführlich heraus. Machen Sie sich keine Sorgen, wenn Sie sich bei der Arbeit mit Versprechungen unsicher fühlen. Es dauert einige Zeit, bis man sich an sie gewöhnt hat. Dies gilt für alle Techniken zum Arbeiten mit asynchronem Code in JS.

Siehe auch „Visualisierung der Arbeit von Service - Mitarbeitern .

Vielen Dank für Ihre Zeit. Ich hoffe es wurde gut angelegt.

All Articles