Asynchrone Funktionen: nutzerfreundliche Versprechen

Mit asynchronen Funktionen können Sie versprechenbasierten Code schreiben, als wäre er synchron.

Jake Archibald
Jake Archibald

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

Unterstützte Browser

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1

Quelle

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:

Nicht empfohlen – zu sequentiell
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Sieht viel übersichtlicher aus, aber der zweite Abruf beginnt erst, wenn der erste Abruf vollständig gelesen wurde, und so weiter. Das ist viel langsamer als das Beispiel mit Promises, bei dem die Abrufe parallel ausgeführt werden. Glücklicherweise gibt es einen idealen Mittelweg.
Empfohlen – schön parallel
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);
  }
}
In diesem Beispiel werden die URLs parallel abgerufen und gelesen, aber das „intelligente“ reduce-Bit wird durch eine standardmäßige, langweilige, lesbare For-Schleife ersetzt.

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

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!