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 się spełni, odzyskasz 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. 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 funkcje wywołania zwrotnego zostały usunięte. Dzięki temu będzie ono znacznie łatwiejsze do odczytania, 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
. Ta obietnica 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ę 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: strumieniowanie odpowiedzi
W bardziej złożonych przykładach korzyści przynoszą funkcje asynchroniczne. 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);
});
});
}
Zobaczcie mi. Jake „władca obietnic” Archibald. 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 wieki, jak w przypadku obrazów 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. 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 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
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: 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. Jednak ta technika kodowania jest na tyle sprytna, że bez niej powinno być lepiej.
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 naprawdę nie chcesz korzystać z transpilatora, możesz użyć polyfill Babel. 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
. 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 skonfigurowanej wersji Babel es2017 i wstępnie skonfigurowanej wersji es2015.
Wyniki nie są tak atrakcyjne, więc uważaj na przerost kodu.
Asynchronicznie wszystko!
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 r. bardzo ucieszyły mnie funkcje asynchroniczne i bardzo się cieszę, że w końcu są dostępne w przeglądarkach. Whoop!