Funkcje asynchroniczne: składanie obietnic przyjazne

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

Jake Archibald
Jake Archibald

Funkcje asynchroniczne są domyślnie włączone w Chrome, Edge, Firefox i Safari i są naprawdę wspaniałe. Umożliwiają one pisanie kodu opartego na obietnicach tak, jakby był on synchroniczny, ale bez blokowania wątku głównego. Dzięki nim asynchroniczny kod jest mniej „sprytny”, ale 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żyjesz słowa kluczowego async, w ramach tej funkcji możesz użyć funkcji await. Gdy await obietnica, funkcja jest wstrzymywana w sposób niezablokowany do czasu jej spełnienia. Jeśli obietnica zostanie spełniona, zwrócimy wartość. Jeśli obietnica zostanie odrzucona, odrzucona wartość zostanie wyrzucona.

Obsługa przeglądarek

Browser Support

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

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

A to ten sam kod, ale z wykorzystaniem 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 nie ma już wywołań zwrotnych. Dzięki temu łatwiej się je czyta, zwłaszcza dla osób, które nie są zaznajomione z obietnicami.

Asynchroniczne wartości zwracane

Funkcje asynchroniczne zawsze zwracają obietnicę, niezależnie od tego, czy używasz await, czy nie. Obiet ten zwraca wartość zwracaną przez funkcję asynchroniczną lub odrzuca wartość zgłaszaną przez funkcję asynchroniczną. 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 obietnicę "world".

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

…wywołanie foo() zwraca obietnicę, która rejects z wartością Error('bar').

Przykład: strumieniowanie odpowiedzi

Zalety funkcji asynchronicznych są coraz bardziej widoczne w bardziej złożonych przykładach. Załóżmy, że chcesz przesłać odpowiedź podczas wylogowywania fragmentów i 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);
    });
  });
}

Poznaj Jake'a „Wypełniającego obietnice” Archibalda. Jak widać, wywołuję funkcję processResult() wewnątrz siebie, aby skonfigurować pętlę asynchroniczną. Pisanie tego tekstu sprawiło, że poczułem się bardzo mądry. Jednak podobnie jak w przypadku większości „inteligentnych” kodów, aby zrozumieć, co się dzieje, trzeba na niego wpatrywać się przez długi czas. Jest to trochę jak z obrazami Magic Eye z lat 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;
}

Nie ma już „inteligentnych” funkcji. Asychronowy cykl, który sprawił, że czułem się tak samozadowolony, został zastąpiony niezawodnym, nudnym cyklem while. O wiele lepiej. W przyszłości otrzymasz asynchroniczne pętle, które zastąpią pętlę while pętlą for-of, co uczyni ją jeszcze bardziej przejrzystą.

Inna składnia funkcji asynchronicznej

Wcześniej pokazałem Ci już async function() {}, ale słowo kluczowe async można też 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 klasy

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

Uwaga! Unikaj zbyt ścisłej kolejności

Mimo że piszesz kod, który wygląda na synchroniczny, nie trać okazji do wykonywania zadań równolegle.

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

Wykonanie tego zadania zajmuje 1000 ms, podczas gdy:

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

Wykonanie tego działania zajmuje 500 ms, ponieważ oba oczekiwania występują w tym samym czasie. Przyjrzyjmy się praktycznemu przykładowi.

Przykład: wyświetlanie danych w kolejności

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

Głębokie oddychanie – tak to wygląda w przypadku obietnic:

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 reduce do łańcucha obietnic. Jestem tak smart. To jednak bardzo sprytny kod, z którego lepiej zrezygnować.

Jednak podczas konwertowania powyższego kodu na funkcję asynchroniczną łatwo jest użyć zbyt sekwencyjnego kodu:

Nie zalecane – zbyt sekwencyjne
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Wygląda to znacznie lepiej, ale drugie pobieranie rozpocznie się dopiero po zakończeniu pierwszego, i tak dalej. Jest to znacznie wolniejsze niż przykład z obietnic, który wykonuje pobieranie w drodze równoległej. Na szczęście istnieje idealny kompromis.
Zalecane – ładne 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 (