Grobe gesagt, bieten Async-Funktionen eine bessere Syntax für Code, der Promises verwendet. Um Async-Funktionen zu verwenden, sollten wir daher Promises verstehen. Sie werden im vorherigen Kapitel erläutert.
Betrachten Sie die folgende Async-Funktion
async function fetchJsonAsync(url) {
try {
const request = await fetch(url); // async
const text = await request.text(); // async
return JSON.parse(text); // sync
}
catch (error) {
assert.fail(error);
}
}Der vorherige, eher synchron wirkende Code ist äquivalent zu folgendem Code, der Promises direkt verwendet
function fetchJsonViaPromises(url) {
return fetch(url) // async
.then(request => request.text()) // async
.then(text => JSON.parse(text)) // sync
.catch(error => {
assert.fail(error);
});
}Einige Beobachtungen zur Async-Funktion fetchJsonAsync()
Async-Funktionen werden mit dem Schlüsselwort async markiert.
Innerhalb des Körpers einer Async-Funktion schreiben wir Promise-basierten Code, als wäre er synchron. Wir müssen den await-Operator nur anwenden, wenn ein Wert ein Promise ist. Dieser Operator pausiert die Async-Funktion und setzt sie fort, sobald das Promise erledigt ist.
await den Erfüllungswert zurück.await den Ablehnungswert.Das Ergebnis einer Async-Funktion ist immer ein Promise.
Sowohl fetchJsonAsync() als auch fetchJsonViaPromises() werden auf exakt die gleiche Weise aufgerufen, wie folgt:
fetchJsonAsync('http://example.com/person.json')
.then(obj => {
assert.deepEqual(obj, {
first: 'Jane',
last: 'Doe',
});
}); Async-Funktionen sind genauso Promise-basiert wie Funktionen, die Promises direkt verwenden
Von außen ist es praktisch unmöglich, den Unterschied zwischen einer Async-Funktion und einer Funktion, die ein Promise zurückgibt, zu erkennen.
JavaScript verfügt über die folgenden asynchronen Versionen von synchronen aufrufbaren Entitäten. Ihre Rollen sind immer entweder echte Funktion oder Methode.
// Async function declaration
async function func1() {}
// Async function expression
const func2 = async function () {};
// Async arrow function
const func3 = async () => {};
// Async method definition in an object literal
const obj = { async m() {} };
// Async method definition in a class definition
class MyClass { async m() {} } Asynchrone Funktionen vs. Async-Funktionen
Der Unterschied zwischen den Begriffen asynchrone Funktion und Async-Funktion ist subtil, aber wichtig.
Eine asynchrone Funktion ist jede Funktion, die ihr Ergebnis asynchron liefert – zum Beispiel eine Callback-basierte Funktion oder eine Promise-basierte Funktion.
Eine Async-Funktion wird über spezielle Syntax definiert, die die Schlüsselwörter async und await beinhaltet. Sie wird aufgrund dieser beiden Schlüsselwörter auch async/await genannt. Async-Funktionen basieren auf Promises und sind daher auch asynchrone Funktionen (was etwas verwirrend ist).
Jede Async-Funktion gibt immer ein Promise zurück.
Innerhalb der Async-Funktion erfüllen wir das Ergebnis-Promise über return (Zeile A).
async function asyncFunc() {
return 123; // (A)
}
asyncFunc()
.then(result => {
assert.equal(result, 123);
});Wie üblich wird, wenn wir nichts explizit zurückgeben, undefined für uns zurückgegeben.
async function asyncFunc() {
}
asyncFunc()
.then(result => {
assert.equal(result, undefined);
});Wir lehnen das Ergebnis-Promise über throw ab (Zeile A).
async function asyncFunc() {
throw new Error('Problem!'); // (A)
}
asyncFunc()
.catch(err => {
assert.deepEqual(err, new Error('Problem!'));
});Wenn wir ein Promise p aus einer Async-Funktion zurückgeben, wird p zum Ergebnis der Funktion (oder genauer gesagt, das Ergebnis „rastet“ bei p ein und verhält sich exakt wie es). Das heißt, das Promise wird nicht in ein weiteres Promise umschlossen.
async function asyncFunc() {
return Promise.resolve('abc');
}
asyncFunc()
.then(result => assert.equal(result, 'abc'));Denken Sie daran, dass jedes Promise q in den folgenden Situationen ähnlich behandelt wird:
resolve(q) innerhalb von new Promise((resolve, reject) => { ··· })return q innerhalb von .then(result => { ··· })return q innerhalb von .catch(err => { ··· })Async-Funktionen werden wie folgt ausgeführt:
p für das Ergebnis wird erstellt, wenn die Async-Funktion gestartet wird.p erledigt wird.return erfüllt p.throw lehnt p ab.q über await gewartet wird. Die Async-Funktion wird pausiert und die Ausführung verlässt sie. Sie wird fortgesetzt, sobald q erledigt ist.p wird zurückgegeben, nachdem die Ausführung den Körper zum ersten Mal verlassen hat (dauerhaft oder vorübergehend).Beachten Sie, dass die Benachrichtigung über die Erledigung des Ergebnisses p asynchron erfolgt, wie es bei Promises immer der Fall ist.
Der folgende Code demonstriert, dass eine Async-Funktion synchron gestartet wird (Zeile A), dann die aktuelle Aufgabe beendet wird (Zeile C) und dann das Ergebnis-Promise asynchron erledigt wird (Zeile B).
async function asyncFunc() {
console.log('asyncFunc() starts'); // (A)
return 'abc';
}
asyncFunc().
then(x => { // (B)
console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)
// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'await: Arbeiten mit PromisesDer await-Operator kann nur innerhalb von Async-Funktionen und asynchronen Generatoren verwendet werden (die in §42.2 „Asynchrone Generatoren“ erläutert werden). Sein Operand ist normalerweise ein Promise und führt zu den folgenden Schritten:
yield in synchronen Generatoren.await den Erfüllungswert zurück.await den Ablehnungswert.Lesen Sie weiter, um mehr darüber zu erfahren, wie await Promises in verschiedenen Zuständen behandelt.
await und erfüllte PromisesWenn sein Operand letztendlich ein erfülltes Promise ist, gibt await seinen Erfüllungswert zurück.
assert.equal(await Promise.resolve('yes!'), 'yes!');Nicht-Promise-Werte sind ebenfalls erlaubt und werden einfach weitergegeben (synchron, ohne die Async-Funktion zu pausieren).
assert.equal(await 'yes!', 'yes!');await und abgelehnte PromisesWenn sein Operand ein abgelehntes Promise ist, wirft await den Ablehnungswert.
try {
await Promise.reject(new Error());
assert.fail(); // we never get here
} catch (e) {
assert.equal(e instanceof Error, true);
} Übung: Fetch API über Async-Funktionen
exercises/async-functions/fetch_json2_test.mjs
await ist flach (wir können es nicht in Callbacks verwenden)Wenn wir uns innerhalb einer Async-Funktion befinden und diese über await pausieren möchten, müssen wir dies direkt innerhalb dieser Funktion tun; wir können es nicht in einer verschachtelten Funktion, wie einem Callback, verwenden. Das heißt, das Pausieren ist flach.
Zum Beispiel kann der folgende Code nicht ausgeführt werden:
async function downloadContent(urls) {
return urls.map((url) => {
return await httpGet(url); // SyntaxError!
});
}Der Grund dafür ist, dass normale Pfeilfunktionen await nicht in ihren Körpern zulassen.
OK, versuchen wir also eine Async-Pfeilfunktion:
async function downloadContent(urls) {
return urls.map(async (url) => {
return await httpGet(url);
});
}Leider funktioniert auch das nicht: Jetzt gibt .map() (und damit downloadContent()) ein Array mit Promises zurück, nicht ein Array mit (entpackten) Werten.
Eine mögliche Lösung ist die Verwendung von Promise.all(), um alle Promises zu entpacken:
async function downloadContent(urls) {
const promiseArray = urls.map(async (url) => {
return await httpGet(url); // (A)
});
return await Promise.all(promiseArray);
}Kann dieser Code verbessert werden? Ja, das kann er: In Zeile A entpacken wir ein Promise über await, nur um es sofort über return wieder zu verpacken. Wenn wir await weglassen, brauchen wir nicht einmal eine Async-Pfeilfunktion.
async function downloadContent(urls) {
const promiseArray = urls.map(
url => httpGet(url));
return await Promise.all(promiseArray); // (B)
}Aus demselben Grund können wir auch await in Zeile B weglassen.
await auf oberster Ebene von Modulen [ES2022]Wir können await auf oberster Ebene von Modulen verwenden – zum Beispiel:
let lodash;
try {
lodash = await import('https://primary.example.com/lodash');
} catch {
lodash = await import('https://secondary.example.com/lodash');
}Weitere Informationen zu dieser Funktion finden Sie in §27.14 „Top-Level await in Modulen [ES2022]“.
Übung: Asynchrones Mapping und Filtern
exercises/async-functions/map_async_test.mjs
Alle verbleibenden Abschnitte sind für Fortgeschrittene.
awaitIn den nächsten beiden Unterabschnitten verwenden wir die Hilfsfunktion paused().
/**
* Resolves after `ms` milliseconds
*/
function delay(ms) {
return new Promise((resolve, _reject) => {
setTimeout(resolve, ms);
});
}
async function paused(id) {
console.log('START ' + id);
await delay(10); // pause
console.log('END ' + id);
return id;
}await: asynchrone Funktionen sequentiell ausführenWenn wir die Aufrufe mehrerer asynchroner Funktionen mit await voranstellen, werden diese Funktionen sequentiell ausgeführt.
async function sequentialAwait() {
const result1 = await paused('first');
assert.equal(result1, 'first');
const result2 = await paused('second');
assert.equal(result2, 'second');
}
// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'Das heißt, paused('second') wird erst gestartet, nachdem paused('first') vollständig abgeschlossen ist.
await: asynchrone Funktionen konkurrierend ausführenWenn wir mehrere Funktionen konkurrierend ausführen möchten, können wir die Tool-Methode Promise.all() verwenden.
async function concurrentPromiseAll() {
const result = await Promise.all([
paused('first'), paused('second')
]);
assert.deepEqual(result, ['first', 'second']);
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'Hier werden beide asynchronen Funktionen gleichzeitig gestartet. Sobald beide erledigt sind, gibt uns await entweder ein Array von Erfüllungswerten oder – wenn mindestens ein Promise abgelehnt wird – eine Ausnahme zurück.
Denken Sie daran aus §40.6.2 „Konkurrenzhinweis: Konzentrieren Sie sich darauf, wann Operationen beginnen“, dass es darauf ankommt, wann wir eine Promise-basierte Berechnung starten; nicht, wie wir ihr Ergebnis verarbeiten. Daher ist der folgende Code genauso „konkurrierend“ wie der vorherige:
async function concurrentAwait() {
const resultPromise1 = paused('first');
const resultPromise2 = paused('second');
assert.equal(await resultPromise1, 'first');
assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'await nicht, wenn wir „feuern und vergessen“await ist bei der Arbeit mit einer Promise-basierten Funktion nicht erforderlich; wir benötigen es nur, wenn wir pausieren und warten möchten, bis das zurückgegebene Promise erledigt ist. Wenn wir nur eine asynchrone Operation starten möchten, dann benötigen wir es nicht.
async function asyncFunc() {
const writer = openFile('someFile.txt');
writer.write('hello'); // don’t wait
writer.write('world'); // don’t wait
await writer.close(); // wait for file to close
}In diesem Code warten wir nicht auf .write(), weil es uns egal ist, wann es beendet ist. Wir möchten jedoch warten, bis .close() abgeschlossen ist.
Hinweis: Jede Ausführung von .write() startet synchron. Das verhindert Race Conditions.
await zu verwenden und das Ergebnis zu ignorierenEs kann gelegentlich sinnvoll sein, await zu verwenden, auch wenn wir sein Ergebnis ignorieren – zum Beispiel:
await longRunningAsyncOperation();
console.log('Done!');Hier verwenden wir await, um uns mit einer lang laufenden asynchronen Operation zu verbinden. Das stellt sicher, dass die Protokollierung wirklich nach Abschluss dieser Operation erfolgt.