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 Chrome, Edge, Firefoksie i Safari. Funkcje te są naprawdę świetne. Umożliwiają pisanie kodu opartego na obietnicach tak, jakby był synchroniczny, ale bez blokowania wątku głównego. Sprawiają, że kod asynchroniczny jest 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żywasz słowa kluczowego async przed definicją funkcji, możesz użyć w niej słowa await. Gdy await obiecuje, funkcja jest wstrzymywana w sposób nieblokujący, dopóki obietnica nie zostanie cofnięta. Jeśli obietnica się spełni, odzyskasz wartość. Jeśli obietnica odrzuci wartość, jest wyświetlana odrzucona wartość.

Obsługiwane przeglądarki

Obsługa przeglądarek

  • 55
  • 15
  • 52
  • 10.1

Źródło

Przykład: rejestrowanie pobierania

Załóżmy, że chcesz pobrać adres URL i zapisać odpowiedź jako tekst. Tak to wygląda z użyciem 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 łatwiej je czytać, zwłaszcza tym mniej zaznajomym z obietnicami.

Asynchroniczne wartości zwracane

Funkcje asynchroniczne zawsze zwracają obietnicę, niezależnie od tego, czy używasz await. Ta obietnica staje się wiążąca z każdym zwracaniem funkcji asynchronicznej lub odrzuca z każdym żądaniem wywołanym przez funkcję asynchroniczną. 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. Załóżmy, że chcesz strumieniowo przesłać odpowiedź, wylogowując fragmenty kodu, a następnie zwrócić ostateczny rozmiar.

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

Zobaczcie mi. Jake „ładny obietnic” Archibald. Zobacz, jak wywołam funkcję processResult() w środku, aby skonfigurować pętlę asynchroniczną? Napisałam pomysł, który sprawił, że poczułam się bardzo sprytna. Ale jak w przypadku większości „inteligentnego” kodu, trzeba wpatrywać się w niego długo, aby zrozumieć, jak działa, jak w przypadku jednego z takich obrazów.

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” zniknęły. Asynchroniczna pętla, która sprawiła, że poczułam się tak zadowolona, została zastąpiona zaufaną, nudną pętlą. Dużo lepiej. W przyszłości będą dostępne iteratory asynchroniczne, które zastąpią pętlę while pętlą for-of, dzięki czemu będzie ona jeszcze bardziej czytelna.

Inna składnia funkcji asynchronicznej

Widzieliśmy już async function() {}, ale słowa kluczowego async można używać 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 przegapisz możliwości wykonywania różnych czynności 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 zapisać je jak najszybciej (we właściwej 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. Jestem tak mądry. Jednak ta technika kodowania jest tak mądra, bez której nie da się już sobie poradzić.

Jednak przy konwertowaniu powyższych parametrów na funkcję asynchroniczną kuszące jest stosowanie zbyt sekwencyjnych:

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 nie rozpocznie się, dopóki moje pierwsze pobieranie nie zostanie w pełni przeczytane itd. To znacznie wolniejsze niż w przykładzie, w którym pobieranie odbywa się równolegle. 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 „inteligentny” bit reduce zostaje 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 (najnowsze wersje wszystkich popularnych przeglądarek), możesz sortować funkcje asynchroniczne polyfill.

Babel zrobi to za Ciebie. Oto przykład z Babel REPL

Zalecamy metodę transpilacji, ponieważ można ją wyłączyć, gdy docelowe przeglądarki obsługują funkcje asynchroniczne. Jeśli jednak naprawdę nie chcesz używać transpilatora, możesz skorzystać z kodu polyfill Babel i użyć go samodzielnie. Zamiast:

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

...uwzględnij parametr polyfill i wpisz:

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

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

Obejście problemu: regenerator

Jeśli kierujesz reklamy na starsze przeglądarki, Babel może też transpilować generatory, co pozwala korzystać z funkcji asynchronicznych aż do IE8. W tym celu potrzebujesz gotowych ustawień Babel es2017 i gotowych ustawień es2015.

Wyniki nie są tak atrakcyjne, więc uważaj na przerost kodu.

Wszystkie funkcje.

Gdy funkcje asynchroniczne pojawią się we wszystkich przeglądarkach, używaj ich w każdej funkcji zwracającej obietnicę. Nie tylko upraszczają one kod, ale też gwarantują, że funkcja zawsze zwróci obietnicę.

W 2014 roku bardzo ekscytowały mnie funkcje asynchroniczne. Bardzo się cieszę, że działają one w przeglądarkach. Oj!