Asynchrone Funktionen: nutzerfreundliche Versprechen

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

Jake Archibald
Jake Archibald

In Chrome, Edge, Firefox und Safari sind asynchrone Funktionen standardmäßig aktiviert und sind wirklich großartig. 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 „klug“ und besser lesbar.

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 du ein Promise mit await festlegst, wird die Funktion nicht blockierend pausiert, bis sich das Promise erfüllt. Wenn sich das Versprechen erfüllt, erhalten Sie den Wert zurück. Wenn das Versprechen abgelehnt wird, wird der abgelehnte Wert ausgegeben.

Unterstützte Browser

Unterstützte Browser

  • 55
  • 15
  • 52
  • 10.1

Quelle

Beispiel: Protokollieren eines Abrufs

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

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

Bei Verwendung asynchroner Funktionen ist das Gleiche dasselbe:

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 identisch, aber alle Callbacks sind verschwunden. Dies erleichtert das 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, was die asynchrone Funktion zurückgibt, aufgelöst oder lehnt es ab, je nachdem, was die asynchrone Funktion 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';
}

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

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

...Der Aufruf von foo() gibt ein Promise zurück, das mit Error('bar') ablehnt.

Beispiel: Antwort streamen

Der Vorteil asynchroner Funktionen steigt 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);
    });
  });
}

Jake, der „Versprechen“ von Archibald. Sehen Sie, wie ich processResult() in sich selbst aufrufe, um eine asynchrone Schleife einzurichten. Durch das Schreiben fühlte ich mich sehr intelligent. Aber wie die meisten "intelligenten" Codes muss man ihn jahrelang anstarren, um herauszufinden, was er tut, wie eines dieser magischen Augenbilder aus den 90er Jahren.

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

Die intelligenten Funktionen sind verschwunden. Die asynchrone Schleife, bei der ich mich so einsam gefühlt hat, wird durch eine vertrauenswürdige, langweilige Schleife ersetzt. Viel besser. In Zukunft erhalten Sie asynchrone Iterationen, die die while-Schleife durch eine For-of-Schleife ersetzen und sie noch übersichtlicher machen.

Syntax anderer asynchroner Funktionen

Ich habe Ihnen bereits async function() {} 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 Verwendung von sequenziellen Anzeigen.

Obwohl Sie Code schreiben, der synchron aussieht, achten Sie darauf, dass Sie nicht die Gelegenheit verpassen, parallele Aufgaben zu erledigen.

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

Der obige Vorgang 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!';
}

Der obige Vorgang dauert 500 ms, da beide Wartezeiten gleichzeitig erfolgen. Sehen wir uns ein Beispiel aus der Praxis an.

Beispiel: Abrufe der Reihe nach 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 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 intelligent, dass Sie ohne sie besser auskommen würden.

Wenn Sie die oben genannte Funktion jedoch in eine asynchrone Funktion umwandeln, ist es verlockend, zu sequenziell zu gehen:

Nicht empfohlen – zu aufeinanderfolgend
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Sieht viel ordentlicher aus, aber der zweite Abruf beginnt erst, wenn mein erster Abruf vollständig gelesen wurde usw. Dies ist viel langsamer als im Versprechen-Beispiel, bei dem die Abrufe parallel durchgeführt werden. Zum Glück gibt es einen idealen Mittelweg.
Empfohlen - 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 standardmäßige, langweilige, lesbare For-Schleife ersetzt.

Behelfslösung für Browserunterstützung: Generatoren

Wenn Ihre Anzeigen auf Browser ausgerichtet sind, die Generatoren unterstützen (die die neueste Version aller gängigen Browser enthalten), können Sie eine Art Asynchrone Polyfill-Funktionen verwenden.

Babel übernimmt das für Sie. Hier finden Sie 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 Babels Polyfill selbst verwenden. Anstelle von:

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

...fügen Sie den Polyfill ein 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 das Ganze gleich.

Problemumgehung: Regenerator

Bei älteren Browsern kann Babel auch Generatoren transpilieren. So können Sie asynchrone Funktionen bis hin zu IE8 verwenden. 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.

Alles asynchron!

Sobald asynchrone Funktionen in allen Browsern verfügbar sind, sollten Sie sie in jeder Funktion verwenden, die vielversprechend ist. Sie machen nicht nur Ihren Code ordentlicher, sondern sorgen auch dafür, dass diese Funktion immer ein Promise zurückgibt.

Ich habe mich schon 2014 so sehr auf asynchrone Funktionen gefreut und es ist toll, dass sie im echten Leben in Browsern landen. Hoppla!