11. Ereignisgesteuerte Programmierung und asynchrone Funktionen

Eine asynchrone Funktion ist eine Funktion, deren Ausführung gestartet wird, auf deren Ergebnis jedoch nicht gewartet wird. Sobald die Ausführung abgeschlossen ist, löst die asynchrone Funktion ein Ereignis aus und übermittelt ihr Ergebnis über dieses Ereignis.
Diese Funktionsweise eignet sich gut für die Ausführung in einem Webbrowser. Denn eine im Browser ausgeführte Anwendung ist eine ereignisgesteuerte Anwendung: Die Anwendung reagiert auf Ereignisse, die hauptsächlich vom Benutzer ausgelöst werden (Klicks, Mausbewegungen, Texteingaben usw.). JavaScript-Anwendungen, die in einem Browser ausgeführt werden, müssen über das Protokoll HTTP mit externen Diensten kommunizieren. Die nativen JavaScript-Funktionen HTTP sind asynchron: Sie werden gestartet, und der Empfang der Antwort des angefragten externen Dienstes wird durch ein Ereignis signalisiert, das zu den von der Anwendung verwalteten Ereignissen hinzugefügt wird.
Die folgenden Skripte werden von [node.js] und nicht von einem Browser ausgeführt. Auch [node.js] verfügt über einen ereignisgesteuerten Ausführungsmodus:
- [node.js] verwendet, ähnlich wie ein Browser, eine Ereignisschleife, um ein Skript auszuführen;
- die Ausführung des Hauptcodes des Skripts ist das erste ausgeführte Ereignis;
- wenn dieser Hauptcode asynchrone Aufgaben gestartet hat, wird die Ausführung fortgesetzt, bis diese asynchronen Aufgaben abgeschlossen sind. Diese lösen ein Ereignis aus, sobald sie abgeschlossen sind. Diese Ereignisse werden in der Ereignisschleife in eine Warteschlange gestellt;
- Das Hauptskript muss diese Ereignisse abonnieren, wenn es die Ergebnisse der asynchronen Aktionen abrufen möchte;
- Das Skript ist erst beendet, wenn alle von ihm ausgelösten Ereignisse verarbeitet wurden;
11.1. Skript [async-01]
Das folgende Skript veranschaulicht das Verhalten eines Skripts mit einer asynchronen Aktion.
'use strict';
// Importe
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// Start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// setTimeout startet einen Timer mit einer Dauer von 1000 ms (2. Parameter) und gibt sofort die Nummer dieses Timers zurück
// Wenn der Timer die 1000 ms abgelaufen sind, löst er ein Ereignis aus, das in die Warteschlange der Laufzeit gestellt wird
// Wenn das Ereignis von der Laufzeitumgebung verarbeitet wird, wird die Funktion (1. Parameter) ausgeführt
setTimeout(function () {
// Dieser Code wird ausgeführt, sobald der Timer den Wert 0 erreicht hat
console.log("[fin de l'action asynchrone setTimeout],", heure(débutScript));
}, 1000)
// wird vor der Meldung der internen Timer-Funktion angezeigt
console.log("[fin du code principal du script],", heure(débutScript));
// Dienstprogramm zur Anzeige von Uhrzeit und Dauer
function heure(début) {
// Aktuelle Uhrzeit
const now = moment(Date.now());
// Zeitformatierung
let result = "heure=" + now.format("HH:mm:ss:SSS");
// Soll eine Dauer berechnet werden?
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
// Formatierung von Uhrzeit und Dauer
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// Ergebnis
return result;
}
- Zeile 4: Die Bibliothek [moment] wird importiert, um die Datumsangaben formatieren zu können (Zeile 27);
- Zeile 5: Die Bibliothek [sprintf-js] wird importiert, um Zeiträume formatieren zu können (Zeile 34);
- Zeile 8: Die Startzeit des Skripts wird notiert;
- Zeile 9: Diese wird mithilfe der Methode [heure] aus den Zeilen 20–34 angezeigt;
- Zeilen 14–17: Die Funktion [setTimeout] hat zwei Parameter (f, Dauer): f ist eine Funktion, die ausgeführt wird, wenn [durée] Millisekunden verstrichen sind;
- Zeile 14: Bei der Ausführung des Skripts wird die Funktion [setTimeout] ausgeführt:
- Zeile 17: Ein Timer von 1000 ms wird gestartet und der Countdown beginnt, bis er später den Wert 0 erreicht. Die Funktion [setTimeout] ist beendet, sobald der Timer initialisiert und der Countdown gestartet wurde. Sie wartet nicht auf das Ende dieses Countdowns. Sie gibt eine Nummer zurück, die den verwendeten Timer identifiziert, und die Ausführung springt zur nächsten Anweisung, Zeile 20. Hier wird das Ergebnis von [setTimeout] nicht verwendet;
- Zeile 16: Diese Meldung wird am Ende der 1000-ms-Verzögerung der Funktion [setTimeout] angezeigt;
- Zeilen 15–16: Die Funktion f, erster Parameter der Funktion [setTimeout], wird nach Ablauf der Verzögerungszeit von 1000 ms ausgeführt. Die Meldung in Zeile 16 wird dann angezeigt;
- Zeile 20: Diese Meldung wird vor der in Zeile 16 angezeigt;
Zeitfunktion:
- Zeile 23: Die Funktion akzeptiert einen optionalen Parameter [heure], der die Startzeit eines Vorgangs angibt, dessen Dauer angezeigt werden soll;
- Zeilen 25–27: Die aktuelle Uhrzeit wird berechnet und formatiert;
- Zeile 29: Ist der Parameter [début] vorhanden, muss eine Dauer berechnet werden;
- Zeile 30: Die Dauer des Vorgangs. Das Ergebnis ist eine Zahl in Millisekunden;
- Zeilen 31–32: Diese Anzahl an Millisekunden wird in Sekunden und Millisekunden zerlegt;
- Zeile 34: Die Dauer wird zur Uhrzeit addiert;
Ausführung
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-01.js"
[début du script], heure=09:26:40:238
[fin du code principal du script], heure=09:26:40:246, durée= 0 seconde(s) et 11 millisecondes
[fin de l'action asynchrone setTimeout], heure=09:26:41:249, durée= 1 seconde(s) et 14 millisecondes
[Done] exited with code=0 in 1.672 seconds
- In Zeile 4 ist zu sehen, dass die asynchrone Aktion [setTimeOut] etwa 1 Sekunde nach dem Ende des Hauptcodes des Skripts abgeschlossen wurde;
- Zeile 6: Die in Zeile 3 angezeigte Uhrzeit ist die des Endes des Hauptcodes. Wenn dieser asynchrone Aufgaben gestartet hat, ist das Skript erst beendet, wenn alle asynchronen Aufgaben ausgeführt wurden. Die in Zeile 6 angezeigte Dauer ist die Gesamtlaufzeit des Skripts (Hauptcode + asynchrone Aufgaben);
Die Funktion [setTimeout] ermöglicht es uns, asynchrone Aufgaben in einer [node.js]-Umgebung zu simulieren. Tatsächlich verhält sich die Funktion [setTimeout] wie eine asynchrone Aufgabe:
- Sie gibt sofort ein Ergebnis zurück – in diesem Fall eine Timer-Nummer – über den üblichen Mechanismus der Funktionen (return);
- sie kann zu einem späteren Zeitpunkt (was oben noch nicht der Fall ist) weitere Ergebnisse über Ereignisse zurückgeben, die dann von der Ereignisschleife von [node.js] verarbeitet werden;
- In den meisten der folgenden Fälle wird es zwei solcher Ereignisse geben:
- ein Ereignis, das man als [success] bezeichnen könnte und das von der asynchronen Aufgabe ausgelöst wird, die ihre Aufgabe erfolgreich ausgeführt hat. Dem ausgelösten Ereignis ist ein Wert – das Ergebnis der Aufgabe – zugeordnet;
- ein Ereignis, das man als [failure] bezeichnen könnte, das von der asynchronen Aufgabe ausgelöst wird, die ihre Aufgabe nicht erfüllen konnte. Dem ausgelösten Ereignis ist ein Datenelement zugeordnet, in der Regel ein Objekt, das den Fehler beschreibt. Mögliche Fehler bei einer asynchronen Internet-Aufgabe wären beispielsweise „Netzwerk nicht verfügbar“, „Server nicht vorhanden“, „Timeout überschritten“ usw.
- Der Hauptcode, der eine asynchrone Aufgabe gestartet hat, kann die Ereignisse abonnieren, die diese Aufgabe möglicherweise auslöst. Wenn eines dieser Ereignisse ausgelöst wird, wird der Hauptcode benachrichtigt und kann die Ausführung einer bestimmten Funktion auslösen, die zur Behandlung des Ereignisses vorgesehen ist. Diese Funktion erhält als Parameter die Daten, die die asynchrone Aufgabe dem ausgelösten Ereignis zugeordnet hat;
11.2. Skript [async-02]
In diesem Skript sendet die asynchrone Funktion [setTimeout] Ereignisse, um Daten an die Codes zu übermitteln, die diese Ereignisse abonniert haben.
Für den Zugriff auf die Ereignisse von [node.js] sind zusätzliche Bibliotheken erforderlich. Wir wählen die Bibliothek [events], die wir zusammen mit [npm] installieren:

Das Skript [async-02] lautet wie folgt:
'use strict';
// Asynchrone Funktionen können ein Ergebnis durch Auslösen eines Ereignisses zurückgeben
// Der Hauptcode kann diese Ergebnisse abrufen, indem er die ausgelösten Ereignisse abonniert
// Importe
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
// Start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// Ein Ereignis-Sender
const eventEmitter = new EventEmitter();
// setTimeout startet einen Timer mit einer Dauer von 1000 ms (2. Parameter) und gibt sofort die Nummer dieses Timers zurück
// Wenn der Timer die 1000 ms abgelaufen sind, sendet er ein Ereignis, das in die Warteschlange der Laufzeitumgebung gestellt wird
// Wenn das Ereignis von der Laufzeitumgebung verarbeitet wird, wird die Funktion (1. Parameter) ausgeführt
setTimeout(function () {
// Dieser Code wird ausgeführt, sobald der Timer den Wert 0 erreicht hat
console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
// Es wird ein Ereignis ausgelöst, um mitzuteilen, dass ein Ergebnis verfügbar ist
eventEmitter.emit("timer1Success", { success: 4 });
// Es wird ein weiteres Ereignis ausgelöst, um mitzuteilen, dass ein weiteres Ergebnis verfügbar ist
eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
// Es wird ein Abonnement für das Ereignis [timer1Success] abgeschlossen
eventEmitter.on('timer1Success', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
// Man abonniert das Ereignis [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});
// wird vor den Meldungen der Ereignisse angezeigt, die von der mit [timer1] verknüpften Funktion ausgegeben werden
console.log("[fin du code principal du script],", heure(débutScript));
// Dienstprogramm zur Anzeige von Uhrzeit und Dauer
function heure(début) {
// aktuelle Uhrzeit
const now = moment(Date.now());
// Zeitformatierung
let result = "heure=" + now.format("HH:mm:ss:SSS");
// Soll eine Dauer berechnet werden?
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
// Formatierung von Uhrzeit und Dauer
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// Ergebnis
return result;
}
Kommentare
- Zeile 9: Hier wird die Klasse [EventEmitter] aus der Bibliothek [events] importiert. Dies ist eine Neuerung: Bislang haben wir nur Literale und Funktionen importiert;
- Zeile 15: Es wird ein Ereignissender [node.js] erstellt, indem die Klasse [EventEmitter] mit dem Schlüsselwort [new] instanziiert wird;
- Zeilen 20–27: Die asynchrone Funktion [setTimeout]. Sie löst bei ihrer Ausführung zwei Ereignisse aus:
- Zeile 24: das Ereignis [timer1Success] mit dem zugehörigen Wert {success : 4};
- Zeile 26: das Ereignis [timer1Failure] mit dem zugehörigen Wert {failure: 6};
- Eine asynchrone Funktion kann beliebig viele Ereignisse auslösen. Wir haben zuvor erwähnt, dass sie in den meisten Fällen eines der beiden Ereignisse [success, failure] auslöst, nicht beide, wie es hier der Fall ist;
- Zeile 20: Die Ausführung von [setTimeout] erfolgt sofort: Ein Timer wird gestartet und dessen Nummer an den aufrufenden Code zurückgegeben. Die Ausgabe der Ereignisse erfolgt später, hier 1 Sekunde später;
- Die Ausgabe von Ereignissen ist sinnlos, wenn es keinen Code gibt, der sie auswertet, sobald sie eintreten. Deshalb muss der Hauptcode beide Ereignisse [timer1Success, timer1Failure] abonnieren, wenn er sie verarbeiten will, insbesondere um die mit diesen Ereignissen verbundenen Daten abzurufen;
- Zeilen 30–32: Der Hauptcode abonniert das Ereignis [timer1Success]. Wenn der Ereignis-Receiver von [node.js] dieses Ereignis verarbeitet, ruft er die Funktion auf, die den zweiten Parameter der Methode [eventEmitter.on] bildet, und übergibt ihr dabei die Daten (hier als [result] bezeichnet), die dem Ereignis [timer1Success] zugeordnet sind;
- Zeile 31: Die Funktion zur Ereignisverarbeitung zeigt den Wert jSON des dem Ereignis zugeordneten Datensatzes sowie die aktuelle Uhrzeit an;
- Zeilen 35–37: Mit einem ähnlichen Code abonniert der Hauptcode das Ereignis [timer1Failure];
- Die Anmeldung zu einem Ereignis (1. Parameter) führt den Code der Funktion [callback] (2. Parameter) nicht sofort aus. Diese wird erst ausgeführt, nachdem das Ereignis stattgefunden hat;
- Zeile 40: Der Hauptcode des Skripts ist beendet, das Skript selbst jedoch noch nicht, da der Hauptcode eine asynchrone Aufgabe gestartet hat. Das gesamte Skript ist erst nach Abschluss dieser asynchronen Aufgabe beendet;
Dies zeigen die erzielten Ergebnisse:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-02.js"
[début du script], heure=09:34:58:909
[fin du code principal du script], heure=09:34:58:916, durée= 0 seconde(s) et 10 millisecondes
[setTimeout, fin du timer d'1 s], heure=09:34:59:929, durée= 1 seconde(s) et 23 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=09:34:59:931, durée= 1 seconde(s) et 25 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=09:34:59:932, durée= 1 seconde(s) et 26 millisecondes, via l'événement [timer1Failure]
[Done] exited with code=0 in 1.627 seconds
- Zeile 3: Ende des Hauptcodes 10 ms nach Beginn des Skripts;
- Zeile 4: Beginn der im 1000-ms-Timer gekapselten Funktion, etwa 1 Sekunde nach dem Start des Skripts;
- Zeile 5: Verarbeitung des Ereignisses [‘timer1Success’], 2 ms später;
- Zeile 6: Verarbeitung des Ereignisses [‘timer1Failure’], 1 ms später als das Ereignis [‘timer1Success’];
- Zeile 8: Ende des gesamten Skripts mit einer Gesamtdauer von 1,627 Sekunden;
11.3. Skript [async-03]
Das folgende Skript zeigt einen weiteren Aspekt der Ereignisschleife von [node.js]:
- Die Schleife führt die Ereignisse nacheinander aus, in der Regel in der Reihenfolge ihres Eintreffens. Einige OS-Skripte weisen den Ereignissen Prioritäten zu, die dann nach Priorität und nicht nach Eintreffsreihenfolge verarbeitet werden;
- Die Schleife führt jeweils nur ein Ereignis aus. Das nächste wird erst verarbeitet, wenn die Verarbeitung des vorherigen abgeschlossen ist. In einem ereignisgesteuerten System sollte man daher vermeiden, Code zu schreiben, der den Prozessor lange Zeit beansprucht, da die Ereignisse dann nicht sofort bei ihrem Auftreten verarbeitet werden, sondern erst später, wenn die Ereignisschleife zu ihnen gelangt. Die Anwendung ist dann wenig „reaktiv“;
Das Skript [async-03] veranschaulicht dieses Phänomen:
'use strict';
// Asynchrone Funktionen können ein Ergebnis durch Auslösen eines Ereignisses zurückgeben
// Der Hauptcode kann diese Ergebnisse abrufen, indem er die ausgelösten Ereignisse abonniert
// Importe
import moment from 'moment';
import { sprintf } from 'sprintf-js';
import EventEmitter from 'events';
// Start
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// Ein Ereignis-Sender
const eventEmitter = new EventEmitter();
// setTimeout startet einen Timer mit einer Dauer von 1000 ms (2. Parameter) und gibt sofort die Nummer dieses Timers zurück
// Wenn der Timer die 1000 ms abgelaufen sind, sendet er ein Ereignis, das in die Warteschlange der Laufzeit gestellt wird
// Wenn das Ereignis von der Laufzeitumgebung verarbeitet wird, wird die Funktion (1. Parameter) ausgeführt
setTimeout(function () {
// Dieser Code wird ausgeführt, sobald der Timer den Wert 0 erreicht hat
console.log("[setTimeout, fin du timer d'1 s],", heure(débutScript));
// Es wird ein Ereignis ausgelöst, um mitzuteilen, dass ein Ergebnis vorliegt
eventEmitter.emit("timer1Success", { success: 4 });
// Es wird ein weiteres Ereignis ausgelöst, um mitzuteilen, dass ein weiteres Ergebnis verfügbar ist
eventEmitter.emit("timer1Failure", { failure: 6 });
}, 1000)
// Man abonniert das Ereignis [timer1Success]
eventEmitter.on('timer1Success', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Success]", result, heure(débutScript)));
});
// Man abonniert das Ereignis [timer1Failure]
eventEmitter.on('timer1Failure', (result) => {
console.log(sprintf("la fonction asynchrone du timer a rendu le résultat [%j], %s, via l'événement [timer1Failure]", result, heure(débutScript)));
});
// Ein etwas ressourcenintensiver synchroner Code, der verhindert hat, dass der Hauptcode vor dem Ende von [timer1] abgeschlossen wurde
for (let i = 0; i < 1000000; i++) {
for (let j = 0; j < 10000; j++) {
i + i ^ 2 + i ^ 3;
}
}
// wird vor den Meldungen der Ereignisse angezeigt, die von der mit [timer1] verknüpften Funktion ausgelöst werden
console.log("[fin du script],", heure(débutScript));
// Dienstprogramm zur Anzeige von Uhrzeit und Dauer
function heure(début) {
...
}
Anmerkungen
- Dieser Code entspricht dem des vorherigen Beispiels [async-02], dem die Zeilen 39–44 hinzugefügt wurden;
- Zeilen 20–27: Die Funktion [setTimeout] wurde so programmiert, dass sie nach einer Verzögerung von einer Sekunde eine interne asynchrone Funktion ausführt. Nach Ablauf dieser Sekunde erfolgt die Ausführung der asynchronen Timer-Funktion nicht sofort: Es wird ein Ereignis in die Ausführungsschleife eingefügt, um diese anzufordern. Ist die Ausführungsschleife gerade mit der Bearbeitung eines anderen Ereignisses beschäftigt, muss die Ausführung der asynchronen Timer-Funktion warten;
- Zeilen 20–27: Sobald die Funktion [setTimeout] ihren Timer mit einer Verzögerung von einer Sekunde gesetzt hat, gibt sie den Prozessor frei und übergibt die Kontrolle an den aufrufenden Code. Dieser fährt mit den Zeilen 30–37 fort, bei denen es sich um Ereignisabonnements handelt, deren Ausführungszeit vernachlässigbar ist;
- Der Hauptcode fährt mit den Zeilen 40–44 fort, die eine Schleife mit 1010 Iterationen bilden. Dieser Code wird gerade ausgeführt, wenn der Timer sein Ereignis „Ende der 1-Sekunden-Verzögerung“ auslöst. Dieses Ereignis wird dann in die Ereignisschleife aufgenommen, muss jedoch das Ende der Ausführung des Hauptcodes des Skripts abwarten, um bearbeitet zu werden;
- Zeile 47: Ende des Hauptcodes des Skripts. Erst nach dieser letzten Ausgabe kann das Timer-Endereignis verarbeitet und die in [setTimeout] integrierte asynchrone Funktion ausgeführt werden;
Das Skript liefert folgende Ergebnisse:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-03.js"
[début du script], heure=08:55:02:665
[fin du code principal du script], heure=08:55:11:789, durée= 9 seconde(s) et 131 millisecondes
[setTimeout, fin du timer d'1 s], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes
la fonction asynchrone du timer a rendu le résultat [{"success":4}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Success]
la fonction asynchrone du timer a rendu le résultat [{"failure":6}], heure=08:55:11:794, durée= 9 seconde(s) et 136 millisecondes, via l'événement [timer1Failure]
[Done] exited with code=0 in 9.796 seconds
Kommentare
- Zeile 3: Man sieht, dass die Ausführung des Hauptcodes des Skripts 9 Sekunden gedauert hat. Die Ereignisse, die während dieser Zeit aufgetreten sind, wurden in der Ereignisschleife in die Warteschlange gestellt;
- Zeile 4: Man sieht, dass das Ereignis [fin du timer] 5 ms nach Beendigung des Hauptcodes verarbeitet wurde. Es wurde etwa 1 s nach Beginn des Skripts ausgelöst, musste jedoch weitere 8 s warten, bis es schließlich verarbeitet wurde;
Aus diesem Beispiel lässt sich ableiten, dass in einem ereignisgesteuerten System ein Code den Prozessor niemals sehr lange beanspruchen darf. Wenn man einen synchronen Code hat, dessen Ausführung lange dauert, muss man ihn in kürzere asynchrone Aufgaben zerlegen, die ihr Ende mit einem Ereignis signalisieren.
11.4. Skript [async-04]
Das Skript [async-04] veranschaulicht einen weiteren Mechanismus, den sogenannten [Promise], ein Ergebnisversprechen. Dieser Mechanismus erspart die explizite Verwaltung von [node.js]-Ereignissen. Dies geschieht implizit, sodass der Entwickler die Existenz dieser Ereignisse ignorieren kann. Ein Verständnis dieser Ereignisse ermöglicht es ihm jedoch, die Funktionsweise von [Promise] besser zu begreifen, die auf den ersten Blick komplex erscheint.
Der Typ [Promise] ist eine JavaScript-Klasse. Ihr Konstruktor akzeptiert als Parameter eine asynchrone Funktion, an die er zwei Parameter übergibt, die traditionell [resolve] und [reject] genannt werden. Sie könnten auch einen anderen Namen tragen;
- Der Konstruktor von [Promise] führt zwei Schritte aus:
- Er erzeugt ein Ereignis, um die Ausführung der Funktion [function(resolve, reject)] auszulösen, die ihm als Parameter übergeben wurde, wartet jedoch nicht auf deren Ergebnis und gibt sofort ein Objekt [Promise] an den aufrufenden Code zurück. Dieses Objekt kann vier Zustände annehmen:
- [pending]: Die asynchrone Aktion, die die Aktion [Promise] ausgelöst hat, ist noch nicht abgeschlossen;
- [fulfilled]: Die asynchrone Aktion, die [Promise] zurückgegeben hat, wurde erfolgreich abgeschlossen;
- [rejected]: Die asynchrone Aktion, die [Promise] zurückgegeben hat, ist fehlgeschlagen;
- [settled]: Die asynchrone Aktion, die [Promise] zurückgegeben hat, ist abgeschlossen;
- Er erzeugt ein Ereignis, um die Ausführung der Funktion [function(resolve, reject)] auszulösen, die ihm als Parameter übergeben wurde, wartet jedoch nicht auf deren Ergebnis und gibt sofort ein Objekt [Promise] an den aufrufenden Code zurück. Dieses Objekt kann vier Zustände annehmen:
Wenn der Konstruktor sein Ergebnis zurückgibt, befindet sich das erstellte Objekt [Promise] im Status [pending] und wartet auf die Ergebnisse der asynchronen Funktion;
- (Fortsetzung)
- Die asynchrone Aufgabe in den Zeilen 2–5 wird sofort gestartet. Bei asynchronen Aufgaben handelt es sich meist um asynchrone Ein-/Ausgabeaufgaben, die sich wie folgt zusammensetzen:
- Ausführung eines synchronen Codes, um den E/A-Vorgang mit einer anderen Komponente, beispielsweise einem Remote-Server, zu starten;
- Warten auf die Antwort dieser Komponente;
- Verarbeitung dieser Antwort;
- Die asynchrone Aufgabe in den Zeilen 2–5 wird sofort gestartet. Bei asynchronen Aufgaben handelt es sich meist um asynchrone Ein-/Ausgabeaufgaben, die sich wie folgt zusammensetzen:
Es ist die Phase 2, das Warten auf die externe Komponente, die den größten Rechenaufwand verursacht. Anstatt zu warten:
- wird der Empfang der angeforderten Daten von der externen Komponente durch ein Ereignis signalisiert;
- im synchronen Code, der auf Phase 1 folgt (Zeile 7 des Beispielcodes), wird man dieses Ereignis abonnieren und zu einem späteren Zeitpunkt in die Ereignisschleife von [node.js] zurückkehren. Das nächste Ereignis in der Liste der ausstehenden Ereignisse wird dann verarbeitet;
- Während Phase 2 findet eine parallele Ausführung statt, jedoch auf verschiedenen Peripheriegeräten:
- der Prozessor für die Ereignisschleife;
- eine externe Komponente (Festplatte, Datenbank, Remote-Server) für die Suche nach den angeforderten Daten;
- Am Ende von Phase 2, wenn der E/A-Vorgang die angeforderten Daten erhalten hat, wird ein Ereignis ausgelöst, um anzuzeigen, dass das Ergebnis der E/A verfügbar ist. Dieses Ereignis reiht sich dann in die Warteschlange der Ereignisse ein;
- Wenn es an der Reihe ist, wird es verarbeitet. Die diesem Ereignis zugeordnete Funktion (Zeile 7 des Beispielcodes) wird dann ausgeführt;
Diese Funktionsweise verhindert Leerlaufzeiten: also Situationen, in denen der Prozessor auf die Antwort eines Peripheriegeräts wartet, das langsamer ist als er selbst;
- (Fortsetzung)
- Wenn die asynchrone Aufgabe aus den Zeilen 2 und 5 gestartet wurde und ihre Arbeit beendet hat, kann sie dem aufrufenden Code ein Ergebnis zurückgeben, und zwar mithilfe der beiden Funktionen [resolve, reject], die der Konstruktor [Promise] ihr als Parameter übergeben hat. Es gilt folgende Konvention:
- Die asynchrone Aufgabe signalisiert einen Erfolg über [resolve(result)]. Dies läuft darauf hinaus, in die Ereignisschleife von [node.js] ein Ereignis einzufügen, das man [resolved] nennen könnte, mit [result] als zugehörigem Datenelement;
- Die asynchrone Aufgabe meldet einen Fehler mit [reject(error)]. Dies läuft darauf hinaus, in die Ereignisschleife von [node.js] ein Ereignis einzufügen, das man als [rejected] bezeichnen könnte, mit [error] als zugehörigem Datenelement – in der Regel ein Objekt, das den aufgetretenen Fehler detailliert beschreibt;
- der aufrufende Code muss sich daher diese beiden Ereignisse abonnieren, um über die Verfügbarkeit des Ergebnisses der asynchronen Funktion informiert zu werden;
- Wenn die asynchrone Aufgabe aus den Zeilen 2 und 5 gestartet wurde und ihre Arbeit beendet hat, kann sie dem aufrufenden Code ein Ergebnis zurückgeben, und zwar mithilfe der beiden Funktionen [resolve, reject], die der Konstruktor [Promise] ihr als Parameter übergeben hat. Es gilt folgende Konvention:
Nach Abschluss der Ausführung der in [Promise] gekapselten asynchronen Aufgabe ändert sich der Status des Objekts [promise], das vom Konstruktor [Promise(…)] zurückgegeben wurde:
- Das Ereignis [resolved] wechselt den Status von [pending] zu [resolved];
- das Ereignis [rejected] versetzt es vom Status [pending] in den Status [rejected];
Das Abonnieren der Ereignisse [resolved] und [rejected] der asynchronen Aufgabe erfolgt mit Methoden der Klasse [Promise] mit folgender Syntax:
promise.then(f1).catch(f2).finally(f3);
wobei:
- f1 ist eine Funktion, die ausgeführt wird, wenn der Status von [promise] von [pending] auf [resolved] wechselt, also wenn die asynchrone Aufgabe ihre Arbeit erfolgreich abgeschlossen hat. Sie erhält als Parameter den Wert [result], der von der Anweisung [resolve(result)] der asynchronen Aufgabe übergeben wird;
- f2 ist eine Funktion, die ausgeführt wird, wenn der Status von [promise] von [pending] auf [rejected] wechselt, also wenn die asynchrone Aufgabe ihre Arbeit nicht erfolgreich ausgeführt hat. Sie erhält als Parameter den Wert [error], der von der Anweisung [reject(errror)] der asynchronen Aufgabe übergeben wird;
- f3 ist eine Funktion, die nach der Ausführung der Methoden [then] oder [catch] ausgeführt wird, also immer ausgeführt wird. Sie erhält keinen Parameter;
Diese Syntax verbirgt die Ereignisse, die abonniert werden, vollständig. Es handelt sich jedoch um ein Abonnement, und wie im vorherigen Beispiel werden die Funktionen [f1, f2, f3] nicht sofort ausgeführt. Diese werden ausgeführt – oder auch nicht –, sobald eines der Ereignisse [resolved, rejected], die abonniert wurden, eintritt.
Das Skript [async-04] veranschaulicht diesen Mechanismus:
'use strict';
// Es ist möglich, die Ergebnisse (Erfolg, Fehler) einer asynchronen Funktion abzurufen
// dank der Klasse [Promise], ohne Ereignisse explizit zu verwenden
// Diese Klasse verwendet implizit Ereignisse, die jedoch im Code nicht sichtbar sind
// Importe
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// Anfang
const débutScript = moment(Date.now());
console.log("[début du script],", heure(débutScript));
// Definition einer asynchronen Aufgabe mithilfe eines Promises [Promise]
// Die asynchrone Aufgabe ist der Konstruktorparameter [Promise]
const débutPromise1 = moment(Date.now());
const promise1 = new Promise(function (resolve) {
// Protokoll
console.log("[début fonction asynchrone de promise1],", heure(débutPromise1));
// asynchroner Code
setTimeout(function () {
console.log("[fin fonction asynchrone de promise1],", heure(débutPromise1));
// Die asynchrone Aufgabe gibt mit der Funktion [resolve] ein Ergebnis zurück
// Das Versprechen ist dann erfolgreich
resolve('[réussite]');
}, 1000)
});
// Das Ergebnis des Promises kann mit der Funktion [promise1] abgerufen werden
// sobald diese aufgelöst (resolve) oder abgelehnt (reject) wurde
// Die folgende Anweisung ist ein Abonnement für das Ereignis [resolved] über die Methode [then]
// sowie für das Ereignis [rejected] über die Methode [catch]
// wird die Methode [finally] ausgeführt, unabhängig davon, ob nach einem „then“ oder einem „catch“
promise1.then(result => {
// im Falle des Erfolgs des Promises [evt resolved]
console.log(sprintf("[promise1.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
// Fehlerfall [evt rejected]
console.log(sprintf("[promise1.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
// wird in jedem Fall ausgeführt
console.log("[promise1.finally]", heure(débutPromise1));
});
// Definition einer asynchronen Aufgabe mithilfe eines Promises [Promise]
const débutPromise2 = moment(Date.now());
const promise2 = new Promise(function (resolve, reject) {
// Protokoll
console.log("[début fonction asynchrone de promise2],", heure(débutPromise1));
// asynchrone Aufgabe
setTimeout(function () {
console.log("[fin fonction asynchrone de promise2],", heure(débutPromise2));
// Die asynchrone Aufgabe gibt mit der Funktion [reject] ein Ergebnis zurück
// Das Versprechen ist dann fehlgeschlagen
reject('[échec]');
}, 2000)
});
// Das Ergebnis des Promises kann mit der Funktion [promise2] abgerufen werden
//, sobald diese aufgelöst (resolve) oder abgelehnt (reject) wurde
promise2.then(result => {
// im Falle eines Erfolgs des Versprechens [evt resolved]
console.log(sprintf("[promise2.then], %s, result=%s", heure(débutPromise2), result));
}).catch(result => {
// im Fehlerfall [evt rejected]
console.log(sprintf("[promise2.catch], %s, result=%s", heure(débutPromise2), result));
}).finally(() => {
// wird in jedem Fall ausgeführt
console.log(sprintf("[promise2.finally], %s", heure(débutPromise2)));
});
// wird vor den Meldungen der asynchronen Funktionen und den zugehörigen Ereignissen angezeigt
console.log("[fin du code principal du script],", heure(débutScript));
// Dienstprogramm
function heure(début) {
// aktuelle Uhrzeit
const now = moment(Date.now());
// Zeitformatierung
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
// Zeitdauer-Formatierung
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// Ergebnis
return result;
}
Kommentare
- Zeilen 18–28: Erstellung eines [Promise promise1]. Seine asynchrone Funktion gibt ihr Ergebnis nach einer Sekunde über ein Ereignis zurück. Sobald dieser asynchrone Vorgang gestartet wurde (Aktivierung eines Timers), wird nicht auf die Rückgabe des Ergebnisses gewartet, sondern es wird sofort mit dem Code in Zeile 35 fortgefahren;
- Zeilen 35–44: Man abonniert die beiden Ereignisse [resolved, rejected], die die in [promise1] enthaltene asynchrone Funktion auslösen kann;
- Zeilen 46–71: Die gleiche Code-Sequenz wie zuvor wird für ein zweites Promise [promise2] wiederholt;
- Zeile 74: Der Hauptcode des Skripts ist beendet, das Skript als Ganzes jedoch noch nicht, da zwei asynchrone Aktionen ausgelöst wurden. Es erfolgt eine Rückkehr zur Ereignisschleife, in der irgendwann eines der Ereignisse [resolved, rejected] der Promises [promise1, promise2] eintreten wird. Dieses wird dann verarbeitet;
- anschließend erfolgt die Rückkehr zur Ereignisschleife. Dort wird das zweite Ereignis [resolved, rejected] der Promises [promise1, promise2] verarbeitet, sobald es eintritt;
Ausführung
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-04.js"
[début du script], heure=09:39:05:950, durée= 0 seconde(s) et 3 millisecondes
[début fonction asynchrone de promise1], heure=09:39:05:958, durée= 0 seconde(s) et 0 millisecondes
[début fonction asynchrone de promise2], heure=09:39:05:959, durée= 0 seconde(s) et 1 millisecondes
[fin du code principal du script], heure=09:39:05:960, durée= 0 seconde(s) et 13 millisecondes
[fin fonction asynchrone de promise1], heure=09:39:06:977, durée= 1 seconde(s) et 19 millisecondes
[promise1.then], heure=09:39:06:980, durée= 1 seconde(s) et 21 millisecondes, result=[réussite]
[promise1.finally] heure=09:39:06:982, durée= 1 seconde(s) et 24 millisecondes
[fin fonction asynchrone de promise2], heure=09:39:07:976, durée= 2 seconde(s) et 17 millisecondes
[promise2.catch], heure=09:39:07:978, durée= 2 seconde(s) et 19 millisecondes, result=[échec]
[promise2.finally], heure=09:39:07:980, durée= 2 seconde(s) et 21 millisecondes
[Done] exited with code=0 in 2.589 seconds
Kommentare
- Zeile 3: Die asynchrone Funktion von [promise1] wird gestartet, aber es wird nicht auf ihr Ende gewartet, das durch ein Ereignis signalisiert wird;
- Zeile 4: Die asynchrone Funktion von [promise2] wird gestartet, aber es wird nicht auf ihr Ende gewartet, das durch ein Ereignis signalisiert wird;
- Zeile 5: Ende des Hauptcodes und Rückkehr zur Ereignisschleife;
- Zeile 6: Verarbeitung des Ereignisses [fin fonction asynchrone de promise1]. Der Status von [promise1] wechselt zu [resolved]. Ein Ereignis signalisiert dies;
- Zeile 7: Da [promise2] seine Arbeit noch nicht abgeschlossen hat, wird das Ereignis [promise1 resolved], das gerade in die Schleife aufgenommen wurde, zunächst von der Methode [promise1.then] und anschließend von der Methode [promise.finally] verarbeitet (Zeile 8);
- Zeilen 9–11: Der gleiche Mechanismus findet statt, wenn [promise2] vom Status [pending] in den Status [resolved] wechselt;
11.5. Skript [async-05]
Kehren wir zum Code des Konstruktors eines Objekts [Promise] zurück:
In Zeile 2 wird die asynchrone Aufgabe von [Promise] gestartet. Diese benötigt oft mehr Parameter als nur die Parameter von [resolve, reject], die ihr von der sie kapselnden Funktion übergeben werden. In diesem Fall wird die Erstellung von [Promise] in eine Funktion gekapselt, die ihr die Parameter übergibt, die ihre asynchrone Funktion benötigt:
Das folgende Skript:
- definiert zwei asynchrone Funktionen, die ein [Promise] zurückgeben;
- startet deren parallele Ausführung und wartet, bis beide abgeschlossen sind, um eine bestimmte Aufgabe auszuführen;
'use strict';
// Man kann asynchrone Funktionen definieren, die einen Typ [Promise] zurückgeben
// Sie können dann mit dem Schlüsselwort [async] gekennzeichnet werden
// Imports
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// Anfang
const débutScript = moment(Date.now());
console.log("[début du script],", heure());
// eine asynchrone Funktion, die ein Promise zurückgibt [Promise]
function async01(p1) {
return new Promise((resolve) => {
console.log("[début de la tâche asynchrone async01]");
// die asynchrone Aufgabe
const débutAsync01 = moment(Date.now());
setTimeout(function () {
console.log("[fin de la tâche asynchrone async01],", heure(débutAsync01));
// Die asynchrone Aufgabe kann ein komplexes Ergebnis zurückgeben
resolve({
prop1: [10, 20, 30],
prop2: "abcd",
prop3: p1,
});
}, 1000)
});
}
// eine asynchrone Funktion, die ein Promise zurückgibt: [Promise]
function async02(p1, p2) {
return new Promise(resolve => {
console.log("[début de la tâche asynchrone async02]");
// asynchrone Aufgabe
const débutAsync02 = moment(Date.now());
setTimeout(function () {
console.log("[fin de la tâche asynchrone async02],", heure(débutAsync02));
// Die asynchrone Aufgabe kann ein komplexes Ergebnis zurückgeben
resolve({
prop1: [11, 21, 31],
prop2: "xyzt",
prop3: p1 + p2
});
}, 2000)
})
}
// beide asynchronen Funktionen werden parallel gestartet
// und wartet, bis beide abgeschlossen sind
// Das „then“ wird nur ausgeführt, wenn beide Funktionen das Ereignis [resolved] ausgelöst haben
// Der „catch“-Block wird ausgeführt, sobald eine der beiden Funktionen das Ereignis [rejected] auslöst
Promise.all([async01(10), async02(10, 20)])
// auslöst; das Ergebnis ist ein Array [result1, result2], wobei [result1] das von einem [resolve] von [async01]
// und [result2] das Ergebnis eines [resolve] von [async02]
.then(result => {
console.log(sprintf("[promise-all success], %s, result=%j", heure(débutScript), result));
})
// error ist das von dem ersten [reject] einer der beiden asynchronen Funktionen ausgegebene Ergebnis
.catch(error => {
console.log(sprintf("[promise-all error], %s, erreur=%j", heure(débutScript), error));
})
// „finally“ wird nach „then“ oder „catch“ ausgeführt
.finally(() => {
console.log(sprintf("[promise-all finally], %s", heure(débutScript)));
});
// wird vor den Meldungen der asynchronen Funktionen und den zugehörigen Ereignissen angezeigt
console.log("[fin du code principal du script],", heure(débutScript));
// Dienstprogramm
function heure(début) {
// aktuelle Uhrzeit
const now = moment(Date.now());
// Zeitformatierung
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
// Zeitdauer-Formatierung
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// Ergebnis
return result;
}
Kommentare
- Zeilen 15–30: Es wird eine Funktion [async01] definiert, die ihr Ergebnis nach 1 Sekunde über ein Timer-Ereignis zurückgibt. Die Funktion [async01] wird in Zeile 26 verwendet;
- Zeilen 33–47: Das Gleiche geschieht mit der Funktion [async02], die ihr Ergebnis nach 2 Sekunden über ein Timer-Ereignis zurückgibt. Die Funktion [async02] akzeptiert zwei Parameter, die in ihrem Ergebnis in Zeile 44 verwendet werden;
- Wenn die beiden Funktionen [async01, async02] aufgerufen werden:
- ausgelöst;
- geben sie dem aufrufenden Code zwei Promises [promise1, promise2] zurück;
- die Ausführung kehrt dann zum aufrufenden Code zurück, der seine Ausführung fortsetzt;
- Nach etwa einer Sekunde löst [async01] ein Ereignis aus, um mitzuteilen, dass sie ihre Arbeit beendet hat. Das betreffende Ereignis wird in der Ereignisschleife in die Warteschlange gestellt, die mit dem von [async01] übermittelten Ergebnis verknüpft ist;
- Nach etwa 2 Sekunden wird derselbe Vorgang für [async02] ablaufen;
- Zeile 54: Erst jetzt werden die asynchronen Funktionen [async01, async02] ausgeführt (Bezeichnungen async01(10) und async02(10,20)). Dies geschieht innerhalb eines Arrays, das als Parameter an die Methode [Promise.all] übergeben wurde. Es ist bekannt, dass [async01, async02] beide ein Promise an den aufrufenden Code zurückgeben. Daher ist der Parameter von [Promise.all] ein Array aus zwei Promises;
- [Promise.all([promise1, promise2, …, promisen]).then(f1).catch(f2).finally(f3)] ist ein Ereignisabonnement:
- [Promise.all] ist vom Typ [Promise];
- die Funktion [f1] der Methode [then] wird ausgeführt, sobald alle Promises [promise1, promise2, …, promisen] aus dem Parameterarray der Methode [all] vomZustand [pending] in den Zustand [resolved] übergegangen sind. Mit anderen Worten: [f1] wird ausgeführt, sobald alle Promises des Arrays erfolgreich abgeschlossen sind;
- die Funktion [f2] der Methode [catch] wird ausgeführt, sobald eines der Promises im Array vom Status [pending] in den Status [rejected] wechselt. Anders ausgedrückt: [f2] wird ausgeführt, sobald eines der Promises im Array fehlschlägt;
- die Funktion [f3] der Methode [finally] wird nach der Ausführung einer der Methoden [then, catch] ausgeführt, also immer ausgeführt;
Die Ausführung des Codes liefert folgende Ergebnisse:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-05.js"
[début du script], heure=12:17:17:367
[début de la tâche asynchrone async01]
[début de la tâche asynchrone async02]
[fin du code principal du script], heure=12:17:17:375, durée= 0 seconde(s) et 10 millisecondes
[fin de la tâche asynchrone async01], heure=12:17:18:391, durée= 1 seconde(s) et 17 millisecondes
[fin de la tâche asynchrone async02], heure=12:17:19:389, durée= 2 seconde(s) et 14 millisecondes
[promise-all success], heure=12:17:19:390, durée= 2 seconde(s) et 25 millisecondes, result=[{"prop1":[10,20,30],"prop2":"abcd","prop3":10},{"prop1":[11,21,31],"prop2":"xyzt","prop3":30}]
[promise-all finally], heure=12:17:19:392, durée= 2 seconde(s) et 27 millisecondes
[Done] exited with code=0 in 2.572 seconds
- Zeilen 6–7: Die beiden asynchronen Aufgaben [async01, async02] werden gestartet. Sie laufen parallel. Nicht die Ausführung ihres Codes erfolgt parallel, sondern ihre jeweiligen Wartezeiten auf die angeforderten Daten finden gleichzeitig statt;
- Zeile 5: Der Hauptcode des Skripts ist abgeschlossen. Nun muss noch das Ende der beiden asynchronen Aufgaben [async01, async02] abgewartet werden;
- Zeile 6: Die asynchrone Aufgabe [async01] wird etwa 1 s nach ihrem Start beendet. Sie gibt ein Ergebnis mit der Funktion [resolve] zurück, sodass ihr Promise im Array in Zeile 56 des Codes vom Status [pending] in den Status [resolved] wechselt. Dies reicht jedoch nicht aus, um die Methode [then] in den Zeilen 59–60 des Codes auszulösen;
- Zeile 7: Die asynchrone Aufgabe [async02] wird etwa 2 s nach ihrem Start beendet. Sie gibt ein Ergebnis mit der Funktion [resolve] zurück, sodass ihr Promise in der Tabelle in Zeile 56 des Codes vom Status [pending] in den Status [resolved] wechselt. Die Methode [then] wird ausgeführt, sobald die Ereignisschleife dies zulässt;
- Zeile 8: Die Methode [then] von [Promise.all] wird ausgeführt. Sie erhält als Parameter ein Array [result1, result2], wobei [result1] das von [async01] ausgegebene Ergebnis ist, und [result2] das von [async02] ausgegebene Ergebnis;
- Zeile 9: Die Methode [finally] von [Promise.all] wird ausgeführt;
11.6. Skript [async-06]
Dieses neue Skript zeigt, wie durch die gemeinsame Verwendung der Schlüsselwörter [async / await] ein asynchroner Code erzielt werden kann, der einem synchronen Code ähnelt. Die Ereignisbehandlung wird vollständig verborgen, was das Verständnis des Codes erleichtert.
Wir greifen das vorherige Beispiel wieder auf und nehmen folgende Änderungen vor:
- Wir fügen eine dritte asynchrone Funktion [async03] hinzu, die ihr Ergebnis mit der Methode [Promise.reject] zurückgibt, wodurch der Ereignisschleife signalisiert wird, dass sie bei der Ausführung ihrer Aufgabe „gescheitert“ ist;
- die drei asynchronen Funktionen [async01, async02, async03] werden sequenziell ausgeführt. Im vorherigen Beispiel wurden die asynchronen Funktionen [async01, async02] parallel ausgeführt;
- Vor dem Aufkommen der Schlüsselwörter [async/await] erfolgte die sequenzielle Ausführung asynchroner Aktionen mithilfe von ineinander verschachtelten [Promise]. Sobald mehrere asynchrone Aktionen auf diese Weise ausgeführt werden mussten, stieg die Anzahl der Promises entsprechend an und der Code wurde weniger lesbar;
- Mit den Schlüsselwörtern [async/await] erfolgt die sequenzielle Ausführung asynchroner Aufgaben mit einer Syntax, die der Ausführung synchroner Aufgaben entspricht:
// asynchrone Funktion – Verwendung von async/await
async function main() {
// Sequentielle Ausführung asynchroner Aufgaben
try {
// Ausführung mit Warten auf [async01]
const result1 = await async01(...);
console.log("[async01 result]=", result1);
// Ausführung mit Warten auf [async02]
const result2 = await async02(...);
console.log("[async02 result]=", result2);
// Ausführung mit Wartezeit auf [async03]
const result3 = await async03(...);
console.log("[async03 result]=", result3);
} catch (error) {
// Eine der asynchronen Aktionen ist fehlgeschlagen
console.log(sprintf("[sequential error]= %j, %s", error));
} finally {
// abgeschlossen
console.log("[fin exécution séquentielle des tâches asynchrones],");
}
- Zeile 6: Die asynchrone Funktion [async01] wird aufgerufen (Schlüsselwort „await“) und es wird gewartet, bis sie ihr Ergebnis über eine der Methoden [Promise.resolve, Promise.reject] veröffentlicht hat. Es handelt sich also um eine blockierende Operation;
- Zeile 6: Das Schlüsselwort [await] wandelt die asynchrone Operation [async01] in eine blockierende Operation um. Es ist bekannt, dass die Operation [async01] ein Ergebnis auf zwei Arten zurückgibt:
- Sie gibt dem aufrufenden Code fast sofort ein Objekt [Promise] zurück;
- sie veröffentlicht zu einem späteren Zeitpunkt ein Ergebnis über die Ereignisschleife mithilfe der Methoden [Promise.resolve, Promise.reject]. Dieses letztere Ergebnis wird von [result1] in Zeile 6 abgerufen. Die Ereignisbehandlung der Aktion [async01] ist nun nicht mehr sichtbar;
- wenn das Ergebnis [result] von [async01] durch [Promise.resolve(result)] veröffentlicht wird, wird es [result1] in Zeile 6 zugewiesen und die Ausführung wird in Zeile 7 fortgesetzt;
- Wenn das Ergebnis von [async01] von [Promise.reject] ausgegeben wird, löst dies eine Ausnahme aus, und die Ausführung des Codes springt zu Zeile 14, der catch-Anweisung. Der Parameter der Klausel [catch] ist das von [async01] mit einem Ausdruck [Promise.reject(error)] veröffentlichte Fehlerobjekt (error). Die asynchrone Aufgabe kann den Fehler auch über einen [throw(error)] veröffentlichen. Das Objekt [error] ist dasjenige, das in [catch(error)] abgerufen wurde;
- das Schlüsselwort [await] muss zwingend in einer Funktion stehen, der das Schlüsselwort [async] in Zeile 2 vorangestellt ist. Dieses Schlüsselwort gibt an, dass die Funktion [main] eine asynchrone Funktion ist;
- im Ausdruck [await f(…)] muss [f] eine asynchrone Funktion sein, die ein Objekt [Promise] an den aufrufenden Code zurückgibt;
- Das Gleiche gilt für die asynchrone Aktion [async02] in Zeile 9 und [async03] in Zeile 12;
Ebenfalls mit den Schlüsselwörtern [async / await] ist die parallele Ausführung asynchroner Aufgaben mit der folgenden Syntax möglich:
try {
// Parallele Ausführung der asynchronen Aufgaben
const result = await Promise.all([async01(...), async02(...), async03(...)]);
console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
} catch (error) {
// Eine der asynchronen Aktionen ist fehlgeschlagen
console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
} finally {
// abgeschlossen
console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
- Zeile 3: Hier handelt es sich um eine blockierende Operation: Es wird gewartet, bis die drei asynchronen Aufgaben des Arrays [async01(..), async02(..), async03(..)] ihre Ergebnisse über eine der Methoden [Promise.resolve, Promise.reject] in der Ereignisschleife veröffentlicht haben;
- Wenn die drei asynchronen Aufgaben ihre Ergebnisse mit [Promise.resolve] veröffentlichen, ist die Konstante [result] das Array [result1, result2, result3], wobei:
- [result1] das von [async01] mit dem Ausdruck [Promise.resolve(result1)] veröffentlichte Ergebnis ist;
- [result2] ist das von [async02] mit dem Ausdruck [Promise.resolve(result2)] veröffentlichte Ergebnis;
- [result3] ist das von [async03] veröffentlichte Ergebnis mit dem Ausdruck [Promise.resolve(result3)];
- Wenn eine der drei Aufgaben ihr Ergebnis mit dem Ausdruck [Promise.reject(error)] veröffentlicht, tritt eine Ausnahme auf;
- Die Konstante [result] in Zeile 3 erhält keinen Wert;
- die Ausführung springt direkt zu [catch] in Zeile 5;
- der Parameter (error) des catch-Blocks ist das vom Ausdruck [Promise.reject(error)] veröffentlichte Objekt (error);
Durch die Kombination dieser beiden Syntaxen lassen sich asynchrone Aufgaben wahlweise sequenziell oder parallel ausführen, und zwar mit einer Syntax, die der eines synchronen Codes ähnelt. Daher sollte diese Syntax bevorzugt werden, da sie wesentlich lesbarer ist als die vorherigen. Diese Syntax [async / await] ist erst ab Version 6 von ECMAScript verfügbar. Es gibt noch immer viele JavaScript-Codes, die [Promise]-Promises verwenden. Deshalb ist es wichtig, auch deren Funktionsweise zu verstehen.
Der vollständige Code des Skripts [async-06] lautet wie folgt:
'use strict';
// Parallele oder sequenzielle Ausführung mehrerer asynchroner Aufgaben
// mit den Schlüsselwörtern „async“ und „await“
// Importe
import moment from 'moment';
import { sprintf } from 'sprintf-js';
// Start
const débutScript = moment(Date.now());
console.log("[début du code principal du script],", heure());
// Eine asynchrone Funktion, die einen [Promise] zurückgibt
function async01(débutAsync01) {
return new Promise(function (resolve) {
console.log("[début fonction asynchrone async01],", heure());
// asynchrone Funktion
setTimeout(function () {
console.log("[fin fonction asynchrone async01],", heure(débutAsync01));
// Die asynchrone Aktion kann ein komplexes Ergebnis zurückgeben
// hier erfolgreich
resolve({
prop1: [11, 21, 31],
prop2: "abcd"
});
}, 1000)
});
}
// eine asynchrone Funktion, die ein [Promise] zurückgibt
function async02(débutAsync02) {
console.log("[début fonction asynchrone async02],", heure());
return new Promise(function (resolve) {
// asynchrone Funktion
setTimeout(function () {
console.log("[fin fonction asynchrone async02],", heure(débutAsync02));
// Die asynchrone Aktion kann ein komplexes Ergebnis liefern
// hier erfolgreich
resolve({
prop1: [12, 22, 32],
prop2: "xyzt"
});
}, 2000)
})
}
// eine asynchrone Funktion, die ein [Promise] zurückgibt
function async03(débutAsync03) {
console.log("[début fonction asynchrone async03],", heure());
return new Promise((resolve, reject) => {
// asynchrone Funktion
setTimeout(function () {
console.log("[fin fonction asynchrone async03],", heure(débutAsync03));
// Die asynchrone Aktion kann ein komplexes Ergebnis liefern
// hier Fehlschlag
reject({
prop1: [13, 23, 33],
prop2: "échec"
});
}, 3000)
})
}
// asynchrone Funktion – Verwendung von async/await
async function main() {
const débutSequential = moment(Date.now());
// Sequentielle Ausführung asynchroner Aufgaben
console.log("------------ exécution séquentielle des tâches asynchrones lancée ------------------------")
try {
// Ausführung mit Warten auf [async01]
const débutAsync01 = moment(Date.now());
const result1 = await async01(débutAsync01);
console.log("[async01 result]=", result1);
// Ausführung mit Warten auf [async02]
const débutAsync02 = moment(Date.now());
console.log("début async02-------------", heure());
const result2 = await async02(débutAsync02);
console.log("[async02 result]=", result2);
// Ausführung mit Wartezeit auf [async03]
const débutAsync03 = moment(Date.now());
console.log("début async03-------------", heure());
const result3 = await async03(débutAsync03);
console.log("[async03 result]=", result3);
} catch (error) {
// Eine der asynchronen Aktionen ist fehlgeschlagen
console.log(sprintf("[sequential error]= %j, %s", error, heure(débutSequential)));
} finally {
// abgeschlossen
console.log("[fin exécution séquentielle des tâches asynchrones],", heure(débutSequential));
}
const débutParallel = moment(Date.now());
// Parallele Ausführung der asynchronen Aufgaben
console.log("------------ exécution parallèle des tâches asynchrones lancée ------------------------")
try {
const result = await Promise.all([async01(débutParallel), async02(débutParallel), async03(débutParallel)]);
console.log(sprintf("[parallel success], %s, result=%j", heure(débutParallel), result));
} catch (error) {
// Eine der asynchronen Aktionen ist fehlgeschlagen
console.log(sprintf("[parallel error], %s, erreur=%j", heure(débutParallel), error));
} finally {
// abgeschlossen
console.log(sprintf("[fin exécution parallèle des tâches asynchrones],%s", heure(débutParallel)));
}
// abgeschlossen
console.log("[fin de la fonction main],", heure(débutSequential));
}
// Ausführung der asynchronen Funktion „main“
main();
// wird vor den verschiedenen Meldungen der asynchronen Funktionen und ihrer Ereignisse angezeigt
console.log("[fin du code principal du script],", heure(débutScript));
// Dienstprogramm
function heure(début) {
// aktuelle Uhrzeit
const now = moment(Date.now());
// Zeitformatierung
let result = "heure=" + now.format("HH:mm:ss:SSS");
if (début) {
const durée = now - début;
const milliseconds = durée % 1000;
const seconds = Math.floor(durée / 1000);
// Zeitformaterung
result = result + sprintf(", durée= %s seconde(s) et %s millisecondes", seconds, milliseconds);
}
// Ergebnis
return result;
}
Die Ergebnisse der Ausführung lauten wie folgt:
[Running] C:\myprograms\laragon-lite\bin\nodejs\node-v10\node.exe -r esm "c:\Data\st-2019\dev\es6\javascript\async\async-06.js"
[début du code principal du script], heure=15:02:00:152
------------ exécution séquentielle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:00:161
[fin du code principal du script], heure=15:02:00:164, durée= 0 seconde(s) et 15 millisecondes
[fin fonction asynchrone async01], heure=15:02:01:165, durée= 1 seconde(s) et 4 millisecondes
[async01 result]= { prop1: [ 11, 21, 31 ], prop2: 'abcd' }
début async02------------- heure=15:02:01:253
[début fonction asynchrone async02], heure=15:02:01:254
[fin fonction asynchrone async02], heure=15:02:03:265, durée= 2 seconde(s) et 12 millisecondes
[async02 result]= { prop1: [ 12, 22, 32 ], prop2: 'xyzt' }
début async03------------- heure=15:02:03:268
[début fonction asynchrone async03], heure=15:02:03:268
[fin fonction asynchrone async03], heure=15:02:06:285, durée= 3 seconde(s) et 18 millisecondes
[sequential error]= {"prop1":[13,23,33],"prop2":"échec"}, heure=15:02:06:289, durée= 6 seconde(s) et 129 millisecondes
[fin exécution séquentielle des tâches asynchrones], heure=15:02:06:291, durée= 6 seconde(s) et 131 millisecondes
------------ exécution parallèle des tâches asynchrones lancée ------------------------
[début fonction asynchrone async01], heure=15:02:06:292
[début fonction asynchrone async02], heure=15:02:06:293
[début fonction asynchrone async03], heure=15:02:06:294
[fin fonction asynchrone async01], heure=15:02:07:294, durée= 1 seconde(s) et 2 millisecondes
[fin fonction asynchrone async02], heure=15:02:08:298, durée= 2 seconde(s) et 6 millisecondes
[fin fonction asynchrone async03], heure=15:02:09:297, durée= 3 seconde(s) et 5 millisecondes
[parallel error], heure=15:02:09:298, durée= 3 seconde(s) et 6 millisecondes, erreur={"prop1":[13,23,33],"prop2":"échec"}
[fin exécution parallèle des tâches asynchrones],heure=15:02:09:299, durée= 3 seconde(s) et 7 millisecondes
[fin de la fonction main], heure=15:02:09:300, durée= 9 seconde(s) et 140 millisecondes
[Done] exited with code=0 in 9.668 seconds