Funkcje asynchroniczne: składanie obietnic przyjazne

Funkcje asynchroniczne umożliwiają pisanie kodu opartego na obietnicach w taki sposób, jakby był synchroniczny.

Jake Archibald
Jake Archibald

Funkcje asynchroniczne są domyślnie włączone w Chrome, Edge, Firefox i Safari, więc działają naprawdę świetnie. Pozwalają pisać kod oparty na obietnicach tak, jakby był synchroniczny, ale bez blokowania wątku głównego. Sprawiają, że kod asynchroniczny jest mniej „sprytny” i bardziej czytelny.

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

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

Jeśli przed definicją funkcji używasz słowa kluczowego async, możesz w niej użyć await. Gdy wyślesz obietnicę await, funkcja zostanie wstrzymana do czasu, aż się ustabilizuje. Jeśli obietnica się spełni, zyskasz w ten sposób wartość. Jeśli obietnica zostanie odrzucona, zwracana jest 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. Oto, jak to wygląda w przypadku obietnic:

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

Tak samo wygląda to 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);
  }
}

Jest taka sama liczba linii, ale wszystkie wywołania zwrotne zostały utracone. Ułatwia to czytanie, zwłaszcza tym osobom, które nie znają się na obietnicach.

Asynchroniczne zwracane wartości

Funkcje asynchroniczne zawsze zwracają obietnicę niezależnie od tego, czy używasz metody await, czy nie. Ta obietnica znika w odniesieniu do wartości zwracanej przez funkcję asynchroniczną lub odrzuca z uwzględnieniem wszystkich funkcji zwracanych przez tę funkcję. W przypadku:

// 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ę, która spełnia wartość "world".

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

...wywołanie foo() zwraca obietnicę, która odrzuca element Error('bar').

Przykład: strumieniowanie odpowiedzi

Korzyści z funkcji asynchronicznych wzrastają w przypadku bardziej złożonych przykładów. Załóżmy, że chcesz przesłać odpowiedź strumieniowo podczas wylogowywania fragmentów i zwrócić ostateczny rozmiar.

Oto kilka obietnic:

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

Spójrzcie na mnie. Jake „posiadający obietnice” Archibald. Zobacz, jak wywołujem w nim funkcję processResult(), by skonfigurować pętlę asynchroniczną. Pisałam, co sprawiło, że czułam się bardzo mądra. Ale jak w przypadku większości „inteligentnych” kodów, trzeba się na niego patrzeć przez wieki, aby się dowiedzieć, do czego służy. To jedno z tych zdjęć z magicznym okiem z lat 90.

Spróbujmy jeszcze raz, używając funkcji asynchronicznych:

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 te „inteligentne” przepadły. Pętla asynchroniczna, która sprawiała, że się czuję, zastąpiliśmy wiarygodna, nudną pętlą. Dużo lepiej. W przyszłości dodamy iterki asynchroniczne, które zastąpią pętlę while pętlą for-of, co sprawi, że pętla będzie jeszcze czytelniejsza.

Inna składnia funkcji asynchronicznej

Pokazaliśmy już nazwę 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 klas

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 nadmiernej sekwencyjności

Choć piszesz kod, który wygląda synchronicznie, pamiętaj, by nie przegapić możliwości wykonywania zadań równolegle.

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

Ta czynność zajmuje 1000 ms, a powyższy:

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

Ta operacja zajmie 500 ms, ponieważ obydwa oczekiwania odbywają się w tym samym czasie. Spójrzmy na przykład praktyczny.

Przykład: ustawianie pobierania w kolejności

Załóżmy, że chcesz pobrać serię adresów URL i zarejestrować je jak najszybciej, we właściwej kolejności.

Głęboki oddech – tak to wygląda z obietnicami:

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 łańcucha sekwencji obietnic. Jestem bardzo bystry. Ale ten kod jest bardzo mądry, bez którego Ci się nie podoba.

Jednak podczas konwertowania powyższych funkcji na funkcję asynchroniczną warto zastosować zbyt sekwencyjną sekwencję:

Niezalecane – zbyt sekwencja
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Wygląda lepiej, ale drugie pobieranie rozpocznie się dopiero wtedy, gdy pierwsze pobieranie zostanie w pełni przeczytane itd. Trwa to znacznie wolniej niż w przykładzie obiecującym, który wykonuje równoległe pobieranie. Na szczęście istnieje idealne rozwiązanie pośrednie.
Polecane – właściwe i równoległe
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 został zastąpiony standardową, nudną i czytelną pętlą for.

Obejście obsługi przez przeglądarkę: generatory

Jeśli kierujesz reklamy na przeglądarki obsługujące generatory (czyli najnowszą wersję każdej popularnej przeglądarki), możesz posortować funkcje asynchroniczne polyfill.

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

Zalecamy transpilację, bo gdy docelowe przeglądarki obsługują funkcje asynchroniczne, wystarczy je wyłączyć. Jeśli jednak naprawdę nie chcesz korzystać z transpilatora, możesz użyć kodu polyfill Babel i zacząć go używać samodzielnie. Zamiast:

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

...wykorzystasz kod polyfill i zapiszesz:

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

Pamiętaj, że musisz przekazać generator (function*) do interfejsu createAsyncFunction i użyć yield zamiast await. Poza tym wszystko działa tak samo.

Obejście: regenerator

Jeśli kierujesz aplikację na starsze przeglądarki, Babel może również transpilować generatory, co pozwala używać funkcji asynchronicznych aż do poziomu IE8. W tym celu potrzebujesz gotowych ustawień es2017 Babel i gotowych ustawień es2015.

Wyniki nie są tak ładne, więc uważaj na duże ilości kodu.

Wszystkie funkcje są zsynchronizowane.

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

Bardzo podoba mi się funkcja asynchronicznych już w 2014 roku. Cieszę się, że pojawiają się one w przeglądarkach. Oj!