JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

41 Async-Funktionen



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.

41.1 Async-Funktionen: die Grundlagen

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()

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.

41.1.1 Async-Konstrukte

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.

41.2 Rückgabe aus Async-Funktionen

41.2.1 Async-Funktionen geben immer Promises zurück

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!'));
});

41.2.2 Zurückgegebene Promises werden nicht umschlossen

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:

41.2.3 Ausführung von Async-Funktionen: synchroner Start, asynchrone Erledigung (Fortgeschritten)

Async-Funktionen werden wie folgt ausgeführt:

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'

41.3 await: Arbeiten mit Promises

Der 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:

Lesen Sie weiter, um mehr darüber zu erfahren, wie await Promises in verschiedenen Zuständen behandelt.

41.3.1 await und erfüllte Promises

Wenn 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!');

41.3.2 await und abgelehnte Promises

Wenn 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

41.3.3 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.

41.3.4 Verwendung von 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

41.4 (Fortgeschritten)

Alle verbleibenden Abschnitte sind für Fortgeschrittene.

41.5 Konkurrenz und await

In 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;
}

41.5.1 await: asynchrone Funktionen sequentiell ausführen

Wenn 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.

41.5.2 await: asynchrone Funktionen konkurrierend ausführen

Wenn 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'

41.6 Tipps zur Verwendung von Async-Funktionen

41.6.1 Wir brauchen 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.

41.6.2 Es kann sinnvoll sein, await zu verwenden und das Ergebnis zu ignorieren

Es 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.