Asynchrone Funktionen: nutzerfreundliche Versprechen

Mit Async-Funktionen können Sie Promise-basierten Code wie synchron schreiben.

Archibald
Jake Archibald

Asynchrone Funktionen sind in Chrome, Edge, Firefox und Safari standardmäßig aktiviert und sie sind ausgesprochen praktisch. Sie ermöglichen es Ihnen, Promise-basierten Code so zu schreiben, als wäre er synchron, aber ohne den Hauptthread zu blockieren. Sie machen Ihren asynchronen Code weniger „clever“ und lesbarer.

Asynchrone Funktionen funktionieren wie folgt:

async function myFirstAsyncFunction() {
  try {
    const fulfilledValue = await promise;
  } catch (rejectedValue) {
    // …
  }
}

Wenn Sie das Schlüsselwort async vor einer Funktionsdefinition verwenden, können Sie await innerhalb der Funktion verwenden. Wenn du ein Promise mit await aktivierst, wird die Funktion auf nicht blockierende Weise pausiert, bis das Versprechen gleich bleibt. Wenn das Versprechen erfüllt, erhalten Sie den Wert zurück. Wenn das Promise ablehnt, wird der abgelehnte Wert ausgelöst.

Unterstützte Browser

Unterstützte Browser

  • 55
  • 15
  • 52
  • 10.1

Quelle

Beispiel: Logging eines Abrufs

Angenommen, Sie möchten eine URL abrufen und die Antwort als Text protokollieren. So sieht das mit Versprechen aus:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

Gleiches gilt für asynchrone Funktionen:

async function logFetch(url) {
  try {
    const response = await fetch(url);
    console.log(await response.text());
  } catch (err) {
    console.log('fetch failed', err);
  }
}

Die Anzahl der Zeilen ist gleich, aber alle Callbacks sind weg. Dadurch ist es leichter zu lesen, insbesondere für diejenigen, die mit Versprechen nicht vertraut sind.

Asynchrone Rückgabewerte

Asynchrone Funktionen geben immer ein Promise zurück, unabhängig davon, ob du await verwendest oder nicht. Dieses Versprechen wird mit dem aufgelöst, was die asynchrone Funktion zurückgibt, bzw. lehnt mit dem ab, was die asynchrone Funktion auslöst. Mit:

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

...das Aufrufen von hello() gibt ein Versprechen zurück, das mit "world" erfüllt wird.

async function foo() {
  await wait(500);
  throw Error('bar');
}

...das Aufrufen von foo() gibt ein Versprechen zurück, das mit Error('bar') abgelehnt wird.

Beispiel: Streaming einer Antwort

Die Vorteile asynchroner Funktionen zeigen sich in komplexeren Beispielen. Angenommen, Sie möchten eine Antwort streamen, während Sie die Blöcke abmelden, und die endgültige Größe zurückgeben.

Hier ist es 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);
    });
  });
}

Schau mal her, Jake, der „Schweiger der Versprechen“ Archibald. Sehen Sie, wie ich processResult() selbst aufrufe, um eine asynchrone Schleife einzurichten? Sehr schlau beim Schreiben. Aber wie die meisten „smarten“ Codes muss man ihn ewig anstarren, um herauszufinden, was er tut, wie z. B. eines dieser Bilder mit magischen Augen aus den 90ern.

Versuchen wir es 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;
}

Alle „Klugen“ sind weg. Die asynchrone Schleife, bei der ich mich so selbstgemacht gefühlt hat, wird durch eine vertrauenswürdige, langweilige Schleife ersetzt. Viel besser. In Zukunft werden asynchrone Iterationen eingeführt, die die while-Schleife durch eine for-of-Schleife ersetzen und somit noch übersichtlicher werden.

Andere Syntax für asynchrone Funktionen

Ich habe Ihnen async function() {} bereits gezeigt, aber das Schlüsselwort async kann mit einer anderen 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 die zu sequenziellen

Auch wenn Sie synchronen Code schreiben, sollten Sie nicht die Gelegenheit verpassen, Dinge parallel auszuführen.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Dies dauert 1.000 ms, während:

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 obigen Vorgangs dauert 500 ms, da beide Wartezeiten gleichzeitig stattfinden. Sehen wir uns ein Beispiel aus der Praxis an.

Beispiel: Abrufe der Reihe nach ausgeben

Angenommen, Sie möchten so schnell wie möglich und in der richtigen Reihenfolge eine Reihe von URLs abrufen und protokollieren.

Tief durchatmen – so sieht das mit 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 schlau. Aber diese Programmierung ist so clever, ohne dass Sie besser dran sind.

Wenn Sie den obigen Befehl in eine asynchrone Funktion umwandeln, ist es jedoch verlockend, zu sequenziell:

Nicht empfohlen – zu sequenziell
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Das sieht viel ordentlicher aus, aber der zweite Abruf beginnt erst, wenn der erste Abruf vollständig gelesen wurde usw. Dies ist viel langsamer als das Versprechen, bei dem die Abrufe parallel ausgeführt werden. Zum Glück 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 „smarte“ reduce-Bit wird durch eine langweilige, lesbare For-Schleife ersetzt.

Behelfslösung für Browsersupport: Generatoren

Wenn Sie Browser auf Browser ansprechen, die Generatoren unterstützen (was die neueste Version aller gängigen Browser enthält), können Sie asynchrone Polyfill-Funktionen verwenden.

Babel erledigt das für Sie, hier ein Beispiel über die Babel-REPL

Ich empfehle den Transpiler-Ansatz, da Sie ihn einfach ausschalten können, sobald Ihre Zielbrowser asynchrone Funktionen unterstützen. Wenn Sie jedoch wirklich keinen Transpiler verwenden möchten, können Sie Babel-Polyfill selbst verwenden. Anstelle von:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

...fügen Sie Polyfill hinzu und schreiben Sie:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Sie müssen einen Generator (function*) an createAsyncFunction übergeben und yield anstelle von await verwenden. Ansonsten funktioniert alles gleich.

Problemumgehung: Regenerator

Babel kann auch Generatoren transpilieren, sodass Sie asynchrone Funktionen bis hin zu IE8 verwenden können. Dazu benötigen Sie die Babel-Voreinstellung es2017 und die Voreinstellung es2015.

Die Ausgabe ist nicht so schön, also achten Sie auf Code-Bloat.

Synchronisieren Sie alle Elemente.

Sobald asynchrone Funktionen in allen Browsern verfügbar sind, verwenden Sie sie in jeder Versprechungsfunktion, die ein Versprechen zurückgeben. Sie sorgen nicht nur dafür, dass Ihr Code übersichtlicher wird, sondern sorgt auch dafür, dass die Funktion immer ein Versprechen zurückgibt.

Asynchrone Funktionen haben mich 2014 wirklich begeistert und es ist toll, zu sehen, wie sie in Browsern landen. Wow!