Funkcje asynchroniczne: składanie obietnic przyjazne

Funkcje asynchroniczne umożliwiają pisanie kodu opartego na obietnicach tak, jakby był on synchroniczny.

Jake Archibald
Jake Archibald

Funkcje asynchroniczne są domyślnie włączone w przeglądarkach Chrome, Edge, Firefox i Safari. są naprawdę cudowne. Umożliwiają one pisanie kodu opartego na obietnicach gdyby był synchroniczny, ale bez blokowania wątku głównego. Sprawiają, że kod asynchroniczny mniej „inteligentny” i bardziej czytelny.

Funkcje asynchroniczne działają w ten sposób:

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

Jeśli użyjesz słowa kluczowego async przed definicją funkcji, możesz użyć funkcji await w funkcji. Gdy await obiecuje, funkcja jest wstrzymywana w sposób nieblokujący, dopóki się nie obiecuje. Jeśli obietnica się spełni, odzyskać wartość. Jeśli obietnica odrzuci wartość, jest wyświetlana odrzucona wartość.

Obsługa przeglądarek

Obsługa przeglądarek

  • Chrome: 55.
  • Krawędź: 15.
  • Firefox: 52.
  • Safari: 10.1

Źródło

Przykład: rejestrowanie pobierania

Załóżmy, że chcesz pobrać adres URL i zapisać odpowiedź jako tekst. Wygląd za pomocą obietnic:

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

To samo przy użyciu funkcji asynchronicznych:

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

Liczba wierszy jest taka sama, ale wszystkie wywołania zwrotne zniknęły. Dzięki temu zwłaszcza dla osób mniej obeznanych z obietnicami.

Asynchroniczne wartości zwracane

Funkcje asynchroniczne zawsze zwracają obietnicę, niezależnie od tego, czy używasz await. Ten obiecuje rozwiązuje się z każdym zwracaniem funkcji asynchronicznej lub odrzuca z argumentem niezależnie od tego, co generuje funkcja asynchroniczna. A więc:

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

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

...wywołanie hello() zwraca obietnicę spełniającą wartość "world".

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

...wywołanie foo() zwraca obietnicę odrzucającą z funkcją Error('bar').

Przykład: przesyłanie strumieniowe odpowiedzi

W bardziej złożonych przykładach korzyści zapewniają funkcje asynchroniczne. Powiedz, że chcesz , aby przesyłać strumieniowo odpowiedź przy jednoczesnym wylogowywaniu fragmentów, i zwracaniu ostatecznego rozmiaru.

Oto obietnice:

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

Obejrzyjcie mnie, Jake „wielder obietnic” Archibald. Zobacz, jak dzwonię processResult() w środku, aby skonfigurować pętlę asynchroniczną? Historie autorstwa że czuję się bardzo mądra. Ale jak większość „inteligentnych” w kodzie, trzeba się na niego patrzeć aby dowiedzieć się, co ona robi, na przykład jedno z tych zdjęć lata 90.

Spróbujmy jeszcze raz z funkcjami asynchronicznymi:

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

Wszystkie „inteligentne” już nie ma aplikacji. Asynchroniczna pętla, która sprawiła, że poczułam się tak rozbawiona. godne zaufania, nudne, „pętla”. Dużo lepiej. W przyszłości możesz iteratory asynchroniczne, co spowodowałoby Zastąp pętlę while pętlą for-of-of, aby stała się jeszcze czystsza.

Inna składnia funkcji asynchronicznej

Widzieliśmy już słowo async function() {}, ale słowo kluczowe async może być bardziej używany z inną składnią funkcji:

Funkcje strzałek

// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
  const response = await fetch(url);
  return response.json();
});

Metody obiektów

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then();

Metody zajęć

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();

Ostrożnie! Unikaj zbyt sekwencyjnego działania

Chociaż piszesz kod, który wygląda na synchroniczny, upewnij się, że nie został pominięty że możemy robić różne rzeczy równolegle.

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

Ten proces zajmuje 1000 ms, a w przypadku:

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

Ukończenie tego procesu zajmuje 500 ms, ponieważ oba oczekiwania odbywają się w tym samym czasie. Spójrzmy na przykład praktyczny.

Przykład: pobieranie danych w określonej kolejności

Załóżmy, że chcesz pobrać serię adresów URL i jak najszybciej je zapisać w w prawidłowej kolejności.

Głęboki oddech – tak z obietnictwem:

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

Tak, używam funkcji reduce do łączenia sekwencji obietnic. jestemwięc . Jednak ta technika kodowania jest tak mądra, bez której nie da się już sobie poradzić.

Jednak przy konwertowaniu powyższych ustawień na funkcję asynchroniczną, kusi zbyt sekwencyjna:

Niezalecane – zbyt sekwencyjne
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
. Wygląda znacznie lepiej, ale drugie pobieranie rozpocznie się dopiero została w pełni przeczytana itd. To znacznie wolniejsze niż w przypadku obiecujących przykładów, umożliwia równoległe pobieranie. Na szczęście istnieje idealne rozwiązanie.
Zalecane – równolegle i ładnie
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);
  }
}
. W tym przykładzie adresy URL są pobierane i odczytywane równolegle, ale „inteligentne” Bit reduce został zastąpiony standardową, nudną, czytelną pętlą for.
.

Obejście obsługi przeglądarki: generatory

Jeśli kierujesz reklamy na przeglądarki, które obsługują generatory (w tym najnowszej wersji każdej popularnej przeglądarki ), możesz posortować funkcje asynchroniczne polyfill.

Babel zrobi to za Ciebie, Oto przykład pochodzący z Babel REPL

.

Zalecamy metodę transpilacji, ponieważ można ją po prostu wyłączyć, docelowe przeglądarki obsługują funkcje asynchroniczne, ale jeśli naprawdę nie chcesz używać funkcji można pobrać kod polyfill Babel's i używać ich samodzielnie. Zamiast:

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

...użyj kodu polyfill i wpisz:

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

Pamiętaj, że musisz przekazać generatora (function*) do: createAsyncFunction, i użyj yield zamiast await. W przeciwnym razie wszystko działa tak samo.

Obejście problemu: regenerator

Jeśli kierujesz kampanię na starsze przeglądarki, Babel może również transpilować generatory, co pozwala korzystać z funkcji asynchronicznych aż do IE8. Aby to zrobić, musisz: Gotowe ustawienia Babel es2017 oraz gotowe ustawienia es2015.

Wyniki nie są tak atrakcyjne, więc uważaj, i nadużywać kodu.

Wszystkie funkcje.

Gdy funkcje asynchroniczne pojawią się we wszystkich przeglądarkach, używaj ich funkcja zwracająca obietnicę! Nie tylko upraszczają one kod, upewnij się, że ta funkcja zawsze zwróci obietnicę.

Bardzo spodobały mi się funkcje asynchroniczne 2014 oraz dobrze jest widzieć, jak lądują w przeglądarkach. Oj!