W tym wpisie opiszę nową propozycje ECMAScript, która wejdzie do standardu ES2018, której już można używać w
przeglądarkach oraz node (wersje przed v10 wymagają opcji --harmony-async-iteration
), jaką jest asynchroniczna
pętla for..of
za pomocą słowa kluczowego await
.
Istnieją jej dwa rodzaje (await
można wstawić w dwóch miejscach):
(async () => {
for (const item of await iterator) {
console.log(item);
}
})()
W tym przypadku iterator to po prostu obietnica, na którą trzeba zaczekać. Jest to odpowiednik:
(async () => {
const value = await iterator;
for (const item of value) {
console.log(item);
}
})();
Drugie użycie to:
(async () => {
for await (const item of iterator) {
console.log(item);
}
})();
Ten przepadek jest już bardziej ciekawy. Aby można było użyć await
w tym miejscu iterator musi zwracać obietnice
wartości, czyli iterujemy po obietnicach. Pętla będzie się zatrzymywać po każdej iteracji dopóki poprzednia wartość
się nie spełni.
Jeśli jesteś zaznajomiony z generatorami i tym że generatory to po prostu cukier syntaktyczny dla iteratorów (jeśli
nie to polecam przeczytać wpis o generatorach i iteratorach), to szczegóły
iteratorów oraz generatorów asynchronicznych nie będą trudne. Iterator asynchroniczny jest podobny do zwykłego,
przy czym kluczem dla iteratora jest Symbol.asyncIterator
(dla normalnego iteratora jest to Symbol.iterator
)
oraz funkcja next
musi zwracać obietnice. Funkcja ta może też być funkcją async
, w której można używać słowa
kluczowego await
, ale nie jest to obowiązkowe.
Najpierw przyjrzyjmy się wnętrznościom czyli iteratorom. Oto przykład zwykłego asynchronicznego iteratora:
function requests(urls) {
urls = urls.slice(); // tworzymy kopie aby nie modyfikować oryginalnej tablicy
return {
[Symbol.asyncIterator]: function() {
return {
next: function() {
var url = urls.shift();
if (url) {
// done jest zbędne, ponieważ w JavaScript undefined jest wartością
// typu false
return fetch(url).then(res => res.text()).then(text => ({value:text}));
}
return {done: true}; // tak jak tutaj value
}
}
}
};
}
ważne jest aby nie zwracać {value: promise}
tylko promise.then(value => ({value}))
.
Tego iteratora można użyć w ten sposób:
(async () => {
var urls = ['https://jcubic.pl', 'https://terminal.jcubic.pl', 'https://jcubic.github.io/git/'];
for await (const text of requests(urls)) {
console.log(text.match(/<title>([^<]+)<\/title>/)[1]);
}
})();
Teraz przykład iteratora, który używa async
. To słowo kluczowe musi być dodane do funkcji next
, ponieważ to ona
jest wywoływana przy każdej iteracji aby zwracać wartość.
function delay(n) {
return new Promise((resolve) => setTimeout(resolve, n));
}
function delayedNumbers(n) {
return {
[Symbol.asyncIterator]: function() {
var i = 0;
return {
next: async function() {
if (i++ < n) {
await delay(1000);
return {value: i};
}
return {done: true};
}
}
}
};
}
(async () => {
for await (let n of delayedNumbers(5)) {
console.log(n);
}
})();
Teraz najlepsze, ponieważ najkrótsze, czyli generatory asynchroniczne:
// funkcja pomocnicza ze stack overflow
function rand(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function* randomNumbers(min, max) {
while (true) {
await delay(1000);
yield rand(min, max);
}
}
tego generatora możemy użyć w ten sposób:
(async () => {
for await (let n of randomNumbers(1, 20)) {
console.log(n);
}
})();
Powyższy kod będzie w nieskończoność produkował losowe liczby, dopóki nie ubijemy procesu lub strony, na której wywoływany jest ten kod.
Tak jak w przypadku zwykłych generatorów, można także używać operatora yield*
, aby „odwijać” inne generatory
wewnątrz generatora.
Należy pamiętać że generatory to tylko cukier syntaktyczny nad iteratorami (dokładnie generator to funkcja, która zwraca iterator). Dlatego popatrz na poniższy kod, w którym korzysta się z generatora tak jak z iteratora, ponieważ udostępnia on to samo API.
async function* titles(urls) {
for (const url of urls) {
const res = await fetch(url);
const text = await res.text();
try {
yield text.match(/<title>([^<]+)<\/title>/)[1];
} catch(e) {
yield null;
}
}
}
var urls = ['https://jcubic.pl', 'https://terminal.jcubic.pl', 'https://jcubic.github.io/git/'];
const iter = titles(urls)[Symbol.asyncIterator]();
iter.next().then(x => console.log(x));
// { value: 'Głównie JavaScript'}
iter.next().then(x => console.log(x));
// { value: 'jQuery Terminal Emulator Plugin' }
iter.next().then(x => console.log(x));
// { value: 'GIT Web Terminal' }
iter.next().then(x => console.log(x));
// { done: true }
Generatory i iteratory asynchroniczne nie są tak straszne jakby się wydawało. W kodzie warto używać generatorów, ale
warto także znać wewnętrzną zasadę ich działania, czyli iteratory. Lista przeglądarek, które wspierają for..await..of
dostępna jest na stronie kangax.github.io/compat-table.
Z chwilą pisania tego artykuły zaimplementowały je Chrome, Firefox oraz Safari.
Komentarze
Hasło, które podasz umożliwi ponowne zalogowanie się i np. usunięcie komentarza, jest dobrowolne. Email jest szyfrowany i używany do wysyłania powiadomień o odpowiedziach do komentarzy oraz do pobierania awatara dzięki usłudze Gravatar.com.