Asynchrone Funktionen: nutzerfreundliche Versprechen

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

Jake Archibald
Jake Archibald

In Chrome, Edge, Firefox und Safari sind asynchrone Funktionen standardmäßig aktiviert. sind sie wirklich fantastisch. Sie ermöglichen es Ihnen, Promise-basierten Code wenn sie synchron wären, aber ohne den Hauptthread zu blockieren. Sie sorgen dafür, asynchroner Code, weniger „clever“ und besser lesbar sind.

So funktionieren asynchrone Funktionen:

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. Wenn du ein Promise mit await aktivierst, wird die Funktion pausiert nicht blockieren, bis sich das Versprechen abwickelt. Wenn das Versprechen erfüllt ist, um den Wert zurückzubekommen. Wenn das Versprechen abgelehnt wird, wird der abgelehnte Wert ausgegeben.

Unterstützte Browser

Unterstützte Browser

  • Chrome: 55 <ph type="x-smartling-placeholder">
  • Rand: 15. <ph type="x-smartling-placeholder">
  • Firefox: 52. <ph type="x-smartling-placeholder">
  • Safari: 10.1 <ph type="x-smartling-placeholder">

Quelle

Beispiel: Protokollieren eines Abrufs

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

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. So können Sie leichter zu lesen, vor allem für diejenigen, die weniger mit Versprechen vertraut sind.

Asynchrone Rückgabewerte

Asynchrone Funktionen geben immer ein Promise zurück, unabhängig davon, ob du await verwendest oder nicht. Das Versprechen mit dem, was die asynchrone Funktion zurückgibt, oder lehnt es mit 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 Promise 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. Sag, du wolltest um eine Antwort zu streamen, während die Blöcke abgemeldet werden, und gibt die endgültige Größe zurück.

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 „wielder of Promis“ (wie viel Versprechen) Archibald. Herausfinden, wie ich anrufe processResult() in sich selbst, um eine asynchrone Schleife einzurichten? Durch das Schreiben fühle ich mich sehr schlau. Aber wie die meisten „smarten“ muss man ihn erst ansehen, um herauszufinden, was es tut, wie z. B. eines dieser magischen Bilder 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;
}

Die cleveren ist verschwunden. Die asynchrone Schleife, die mich so selbstbewusst gefühlt hat, durch eine vertrauenswürdige, langweilige Während-Schleife ersetzt. Viel besser. Zukünftig erhalten Sie asynchrone Iterationen Das würde ersetze die while-Schleife durch eine For-of-Schleife, um sie noch übersichtlicher zu gestalten.

Syntax anderer asynchroner Funktionen

Ich habe Ihnen bereits async function() {} gezeigt, aber das Keyword async kann zusammen mit einer anderen Funktionssyntax verwendet:

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, stellen Sie sicher, dass Sie den Code die Möglichkeit, Dinge parallel zu tun.

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 diese so schnell wie möglich in der in der richtigen Reihenfolge.

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 intelligent. Aber diese Programmierung ist so intelligent, dass Sie ohne sie besser auskommen würden.

Bei der Konvertierung der obigen Anweisungen in eine asynchrone Funktion ist es jedoch verlockend, zu sequenziell:

Nicht empfohlen – zu aufeinanderfolgend
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
<ph type="x-smartling-placeholder"></ph> Sieht ordentlicher aus, aber der zweite Abruf beginnt erst, vollständig gelesen wurden usw. Das ist viel langsamer als das Beispiel, und führt die Abrufe parallel aus. 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);
  }
}
<ph type="x-smartling-placeholder"></ph> In diesem Beispiel werden die URLs parallel abgerufen und gelesen, Das reduce-Bit wird durch eine langweilige Standardschleife ersetzt, die für eine Schleife lesbar ist.

Behelfslösung für Browserunterstützung: Generatoren

Wenn Sie Ihre Anzeigen auf Browser ausrichten, die Generatoren unterstützen (einschließlich der aktuellen Version jedes gängigen Browsers ) können Sie asynchrone Polyfill-Funktionen verwenden.

Babel übernimmt das für Sie. ein Beispiel aus der Babel-REPL

Ich empfehle den Transpiler-Ansatz, da Sie ihn einfach deaktivieren können, sobald Sie Zielbrowser unterstützen asynchrone Funktionen. Wenn Sie jedoch wirklich keine können Sie Polyfill von Babel und verwenden Sie es selbst. Anstelle von:

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

...brauchst du den Polyfill und schreibe:

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

Du musst einen Generator (function*) an createAsyncFunction übergeben. und verwende yield statt await. Ansonsten funktioniert das Ganze gleich.

Problemumgehung: Regenerator

Bei älteren Browsern kann Babel auch Generatoren transpilieren, Sie können asynchrone Funktionen bis hin zu IE8 verwenden. Dazu benötigen Sie Babel-Voreinstellung es2017 und der Voreinstellung „es2015“.

Die Ausgabe ist nicht so hübsch, also achten Sie auf aufgeblähter Code.

Alles asynchron!

Sobald asynchrone Funktionen in allen Browsern verfügbar sind, sollten Sie sie auf jedem versprochene Funktion! Sie machen Ihren Code nicht nur ordentlicher, sondern kannst du dafür sorgen, dass diese Funktion immer ein Versprechen zurückgibt.

Ich war schon sehr gespannt auf asynchrone Funktionen 2014 und ist es toll, wenn sie im echten Leben im Browser landen. Hoppla!