Befor es in JS "Promise" gab hat man Callback Funktionen genutzt. Also
man übergab einer Funktion eine Callback Funktion, die am Ende
aufgerufen wurde, normalerweise mit dem Resultat oder einem Fehler.
Dann kamen die Promise Objekte. Das ist eine gute Abstraktion davon, die
das konsistenter macht. Dort kann man mit then() die Callback Funktion
übergeben. Man hat 2, eine für Fehler, und eine für wenn alles OK ist.
Die Promise kann das Resultat speichern, und es kennt die Zustände
ausstehend, fertig, oder Fehler.
Bei "new Promise" kann man eine Funktion übergeben, die sofort
aufgerufen wird, und wenn sie fertig ist, den an sie übergebenen
Callback aufruft, der dann den Wert der Promise setzt. Das ist
eigentlich wie früher mit den Callbacks.
Man kann zwischen den 2 Varianten, Callback übergeben und Promise zurück
geben, Konvertieren, in nodejs gibt es dafür fertige Funktionen
"util.promisify" und "util.callbackify". In dem sinne ist das
äquivalent.
So ein Promise Objekt hat aber noch einige Vorteile. Man kann z.B. auf
mehrere Ergebnisse auf einmal warten, und weil man beim Callback, das
man an then() übergibt, ein Promise Objekt übergeben kann, kommt man
ohne tief verschachtelte Callbacks aus. Und bei den Callbacks die man
der then() Funktion übergibt, hat man das Problem nicht, dass man einen
Fehler haben könnte, und vergisst, den Abzufangen und den reject
Callback aufzurufen, dass kann die then() funktion auch.
Hier gibt es nun noch etwas spezielles, bei den Promise Objekten. In JS
gibt es das Konzept eines Objekts, dass then-able ist. Das ist jedes
Objekt, dass eine then funktion hat, der ein Callback übergeben wird,
der dann aufgerufen wird. Das ist unter anderem für nicht native Promise
Implementierungen wichtig, was bei der Einführung wichtig war. Eine
Promise ist ein then-able Objekt, und then-able Objekte werden wie
Promises behandelt. Man kann also z.B. sowas machen:
1 | Promise.resolve({then(resolve){ setTimeout(resolve, 1000); }})
|
2 | .then(()=>{console.log("I was called 1 second later");});
|
Das ganze war manchmal aber immer noch fehleranfällig, und nicht ganz so
praktisch wie möglich. z.B. hatte man bei "new Promise" immer noch
callbacks, die man vergessen könnte, aufzurufen (und die Variante mit
den then-ablen Objekten von oben kannten die wenigsten). Deshalb wurden
async Functionen, und das await Keyword eingeführt.
Eine async Funktion gibt eine Promise zurück. Und immer eine Native, man
kann die nicht ersetzen*. Gibt es eine Exception, wird die Promise immer
rejected, da kann man (fast) nichts falsch machen. Und nur in den async
Funktionen, gibt es das await keyword. Laut spec wartet man damit auf
Promises. Tatsächlich wird aber einfach die then() Funktion aufgerufen,
und dieser eine Continuation der then() Funktion übergeben.
1 | (async()=>{
|
2 | await { then(continuation){ setTimeout(continuation, 1000); } };
|
3 | console.log("I was called 1 second later");
|
4 | })();
|
Die Continuation ist wie eine simple callback Funktion, die die Async
funktion fortsetzt. Sie wird aber niemals synchron aufgerufen:
1 | (async()=>{
|
2 | console.log(1);
|
3 | console.log(await {
|
4 | then(continuation){
|
5 | console.log(2);
|
6 | continuation("result");
|
7 | console.log(3);
|
8 | }
|
9 | });
|
10 | console.log(4);
|
11 | })();
|
Ausgabe:
Wie hier zu sehen ist, wird continuation nicht sofort ausgeführt,
sondern erst beim nächsten event loop Durchgang.
Letztendlich kommt es aber aufs gleiche raus. Man kann auf eine Promise
Warten.
Das heisst aber auch, dass bei jedem Await, eine Funktion unterbrochen,
und andere Funktionen im event loop aufgerufen werden können. Aber nur
dort, die restlichen teile der Funktion werden niemals unterbrochen.
Ein paar Fallstricke hat das auch, aber es ist viel einfacher damit
umzugehen als beim Multithreading, und manchmal kann man das sogar
anderweitig ausnutzen.
Zu guter letzt, der Vollständigkeit halber, sind noch Generator
Funktionen zu erwähnen. Die gab es schon lange vor Promise Objekten und
vor async-await. Dort werden die Funktionen beim "yield" Keyword
unterbrochen, und das beim Aufruf des Generators zurückgegebene Objekt
hat eine next() Funktion, quasi aine continuation, das führt die
Funktion fort, und man kann das yield so sogar werte zurückgeben lassen.
1 | function* mygenerator(){
|
2 | console.log(1);
|
3 | console.log(yield 2);
|
4 | console.log(yield 3);
|
5 | }
|
6 | var x = mygenerator();
|
7 | console.log(0);
|
8 | console.log(x.next("a"));
|
9 | console.log(x.next("b"));
|
10 | console.log(x.next("c"));
|
Ausgabe:
1 | 0
|
2 | 1
|
3 | { value: 2, done: false }
|
4 | b
|
5 | { value: 3, done: false }
|
6 | c
|
7 | { value: undefined, done: true }
|
In gewissem Sinne ist das analog zu async Funktionen, man könnte die
damit simulieren, das wäre aber weder Sinvoll noch Praktisch. Sind dafür
für anderes gut. So ein Generator kann man auch mit for(x of g) nutzen.
Und das lässt sich auch mit async kombinieren, nennt sich dann async
generatoren. Die kann man mit "for await(x of ag)" iterieren.
Da gäbe es noch viel mehr zu sagen, aber das dürfte alles wichtige
zusammenfassen.