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.