Mit asynchronen Funktionen können Sie versprechenbasierten Code schreiben, als wäre er synchron.
Async-Funktionen sind in Chrome, Edge, Firefox und Safari standardmäßig aktiviert und sind ganz ehrlich gesagt großartig. Sie ermöglichen es, versprechenbasierten Code so zu schreiben, als wäre er synchron, ohne den Hauptthread zu blockieren. Sie machen Ihren asynchronen Code weniger „schlau“ und lesbarer.
So funktionieren asynchrone Funktionen:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
Wenn Sie das Schlüsselwort async
vor einer Funktionsdefinition einsetzen, können Sie innerhalb der Funktion await
verwenden. Wenn Sie ein Versprechen await
, wird die Funktion nicht blockierend pausiert, bis das Versprechen erfüllt ist. Wenn das Versprechen erfüllt wird, erhalten Sie den Wert zurück. Wenn das Versprechen abgelehnt wird, wird der abgelehnte Wert geworfen.
Unterstützte Browser
Beispiel: Abruf protokollieren
Angenommen, Sie möchten eine URL abrufen und die Antwort als Text protokollieren. Mit Promises sieht das so aus:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
Und hier dasselbe mit asynchronen Funktionen:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
Es gibt dieselbe Anzahl von Zeilen, aber alle Rückrufe sind verschwunden. Das macht das Lesen viel einfacher, vor allem für diejenigen, die mit Versprechen weniger vertraut sind.
Asynchrone Rückgabewerte
Asynchrone Funktionen geben immer ein Versprechen zurück, unabhängig davon, ob Sie await
verwenden oder nicht. Dieses Versprechen wird mit dem Wert aufgelöst, den die asynchrone Funktion zurückgibt, oder abgelehnt, wenn die asynchrone Funktion eine Ausnahme auslöst. Also bei:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
…wird durch den Aufruf von hello()
ein Versprechen zurückgegeben, das mit "world"
erfüllt wird.
async function foo() {
await wait(500);
throw Error('bar');
}
…wird durch den Aufruf von foo()
ein Versprechen zurückgegeben, das mit Error('bar')
abgelehnt wird.
Beispiel: Streaming einer Antwort
Bei komplexeren Beispielen sind asynchrone Funktionen noch effektiver. Angenommen, Sie möchten eine Antwort streamen, während Sie die Chunks protokollieren und die endgültige Größe zurückgeben.
Hier ist sie mit Versprechen:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
Ich bin Jake „der Versprechensmacher“ Archibald. Sehen Sie, wie ich processResult()
in sich selbst aufrufe, um eine asynchrone Schleife einzurichten? Texte, die mich sehr klug fühlen lassen. Aber wie bei den meisten „intelligenten“ Codes müssen Sie sich die Sache lange ansehen, um herauszufinden, was er tut, ähnlich wie bei einem Magic-Eye-Bild aus den 90ern.
Versuchen wir das noch einmal mit asynchronen Funktionen:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
Das „Smart“ ist weg. Die asynchrone Schleife, bei der ich mich so einsam gefühlt hat,
wird durch eine vertrauenswürdige, langweilige Schleife ersetzt. Viel besser. Künftig gibt es asynchone Iteratoren, mit denen die while
-Schleife durch eine for-of-Schleife ersetzt wird. Das macht den Code noch übersichtlicher.
Syntax anderer asynchroner Funktionen
Ich habe Ihnen bereits async function() {}
gezeigt, aber das Schlüsselwort async
kann auch mit anderer Funktionssyntax verwendet werden:
Pfeilfunktionen
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Objektmethoden
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
Klassenmethoden
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
Vorsicht! Vermeiden Sie eine zu sequentielle Struktur.
Auch wenn Sie Code schreiben, der synchron aussieht, sollten Sie die Möglichkeit nicht verpassen, Dinge parallel auszuführen.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
Die Ausführung des obigen Codes dauert 1.000 Millisekunden. Bei folgendem Code:
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
Die Ausführung des oben genannten Codes dauert 500 ms, da beide Wartezeiten gleichzeitig ablaufen. Sehen wir uns ein Beispiel aus der Praxis an.
Beispiel: Abrufe in der richtigen Reihenfolge ausgeben
Angenommen, Sie möchten eine Reihe von URLs abrufen und so schnell wie möglich in der richtigen Reihenfolge protokollieren.
Tief durchatmen: So sieht das bei Versprechen aus:
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
Ja, das stimmt. Ich verwende reduce
, um eine Reihe von Versprechen zu verketten. Ich bin so klug. Aber das ist ein bisschen zu smart, um es zu verwenden.
Wenn Sie jedoch die obigen Anweisungen in eine asynchrone Funktion umwandeln, ist es verlockend, zu sequenziell zu gehen:
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
function markHandled(...promises) {
Promise.allSettled(promises);
}
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.text();
});
markHandled(...textPromises);
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
Behelfslösung für Browserunterstützung: Generatoren
Wenn Sie Ihre Website auf Browser ausrichten, die Generatoren unterstützen (einschließlich der neuesten Versionen aller gängigen Browser), können Sie asynchrone Funktionen in gewisser Weise polyfillen.
Babel übernimmt das für Sie. Hier ein Beispiel über die Babel-REPL
- Beachten Sie, wie ähnlich der transpilierte Code ist. Diese Transformation ist Teil der es2017-Voreinstellung von Babel.
Ich empfehle den Transpilierungsansatz, da Sie ihn einfach deaktivieren können, sobald Ihre Zielbrowser asynchrone Funktionen unterstützen. Wenn Sie wirklich keinen Transpiler verwenden möchten, können Sie die Polyfill-Funktion von Babel selbst verwenden. Anstelle von:
async function slowEcho(val) {
await wait(1000);
return val;
}
… würden Sie die Polyfill-Funktion einbinden und Folgendes schreiben:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Sie müssen createAsyncFunction
einen Generator (function*
) übergeben und yield
anstelle von await
verwenden. Ansonsten funktioniert es genauso.
Problemumgehung: regenerator
Wenn Sie Ihre Website auf ältere Browser ausrichten, kann Babel auch Generatoren transpilieren. So können Sie asynchrone Funktionen bis hinunter zu IE 8 verwenden. Dazu benötigen Sie das Babel-Preset „es2017“ und das Preset „es2015“.
Die Ausgabe ist nicht so schön, achten Sie also auf Code-Bloat.
Alles asynchron!
Sobald asynchrone Funktionen in allen Browsern verfügbar sind, sollten Sie sie für alle Funktionen verwenden, die ein Versprechen zurückgeben. Sie sorgen nicht nur für einen ordentlichen Code, sondern sorgen auch dafür, dass diese Funktion immer ein Promise zurückgibt.
2014 war ich schon sehr begeistert von asynchronen Funktionen und es ist toll, dass sie jetzt in Browsern eingesetzt werden. Juhu!