Funkcje asynchroniczne umożliwiają tworzenie kodu opartego na obietnicach tak, jakby był on synchroniczny.
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”, a 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
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 wszystkie funkcje wywołania zwrotnego zostały usunięte. Dzięki temu będzie ono znacznie łatwiejsze w czytaniu, 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 na obietnicy jest rozwiązywany z wartością zwracaną przez funkcję asynchroniczną lub odrzucany z wartością rzucaną 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 odrzuca 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. Czy widzisz, że 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. Asymetryczna pętla, która sprawiła, że czułem się tak samolubny, została zastąpiona przez niezawodną, nudną pętlę while. O wiele lepiej. W przyszłości otrzymasz asynchroniczne liczniki, 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
Podczas pisania kodu, który wygląda na synchroniczny, nie zapomnij o możliwości 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: wyprowadzanie 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 podejścia:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
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); } }
Obsługa przeglądarek: generatory
Jeśli kierujesz reklamy na przeglądarki obsługujące generatory (w tym najnowszą wersję każdej głównej przeglądarki), możesz w pewien sposób polyfillować funkcje asynchroniczne.
Babel zrobi to za Ciebie. Oto przykład w Babel REPL:
- zwróć uwagę, jak podobny jest skompilowany kod. Ta transformacja jest częścią wstępnie ustawionego Babela es2017.
Polecam podejście polegające na transpilacji, ponieważ możesz je wyłączyć, gdy przeglądarki docelowe będą obsługiwać funkcje asynchroniczne. Jeśli jednak naprawdę nie chcesz korzystać z transpilatora, możesz użyć polyfill Babel. Zamiast:
async function slowEcho(val) {
await wait(1000);
return val;
}
…uwzględnij polyfill i napisz:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Pamiętaj, że musisz przekazać generator (function*
) do createAsyncFunction
, a zamiast await
musisz użyć yield
. Poza tym działa tak samo.
Rozwiązanie: regenerator
Jeśli kierujesz się na starsze przeglądarki, Babel może też transpilować generatory, co pozwala używać funkcji asynchronicznych aż do IE8. Aby to zrobić, musisz użyć wstępnie ustawionego profilu es2017 w Babelu oraz wstępnie ustawionego profilu es2015.
Dane wyjściowe nie są tak ładne, więc uważaj na błąd typu code-bloat.
Asynchronicznie wszystko!
Gdy asynchroniczne funkcje będą dostępne we wszystkich przeglądarkach, używaj ich we wszystkich funkcjach zwracających obietnicę. Nie tylko sprawiają, że kod jest bardziej uporządkowany, ale też zapewniają, że funkcja zawsze zwróci obietnicę.
W 2014 r. bardzo ucieszyły mnie funkcje asynchroniczne i bardzo się cieszę, że w końcu są dostępne w przeglądarkach. Whoop!