Asynchroniczne generatory i pętla for-await
Słowem wstępu
Asynchroniczność to nieodłączny element języka JavaScript, bez niego tworzenie nowoczesnych, dynamicznych aplikacji byłoby praktycznie niemożliwe, ponieważ pozwala wykonywać operacje czekając na ich rezultat, nie blokując tym samym działania całej aplikacji.
Generatory natomiast, to stosunkowo nowa funkcjonalność tego języka, dodana w specyfikacji ES2015. Nie cieszą się jednak dużą popularnością i mogą wydawać się trochę "magiczne".
Jako przykład użycia tych funkcjonalności posłuży nam stworzenie prostej aplikacji, która będzie symulować proces migracji bazy danych, gdzie wszystkie skrypty muszą wykonać się po kolei, a wyłapany błąd zakończy jej pracę, aby zachować poprawną strukturę bazy.
Zanim zaczniemy...
- Skryptami migracji na potrzeby przykładu będą proste funkcje asynchroniczne
- Funkcja może nie wykonać się poprawnie np. z powodu timeoutu, błędu w pliku migracji czy nawet w samym kodzie - musimy wtedy przerwać migrację, aby zachować spójność danych
Zaczynamy!
Poniższa funkcja zwróci Promise i będzie symulować wykonywanie się skryptu migracji.
Przyjęliśmy, że każda operacja trwa 1 sekundę i możemy określić czy ma wykonać się poprawnie czy nie (podając parametr rejected
).
const createMigration = (name, rejected = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (rejected) {
return reject(`[fail] ${name}`);
}
resolve(`[success] ${name}`);
}, 1000);
});
};
Następnie stwórzmy kilka migracji
const migrations = [
() => createMigration('initial'),
() => createMigration('addTable'),
() => createMigration('addSomeField', true), // ta migracja nie wykona się poprawnie
() => createMigration('removeSomeField'),
];
Dlaczego w tablicy migrations
każdy z elementów to funkcja zwracająca Promise, a nie po prostu Promise jak poniżej?
const migrations = [
createMigration('initial'),
createMigration('addTable'),
createMigration('addSomeField', true),
createMigration('removeSomeField'),
];
Dla powyższego, interpreter JS ustawi status wszystkich Promise' ów na pending
w momencie wykonywania się kodu linia po linii (mówiąc prosto: uruchomi je). W efekcie czego, nie będziemy mieć kontroli nad tym, który kiedy się wykona i co najważniejsze, czy zrobi to poprawnie.
Nasz generator
async function* runMigrations() {
for(migration of migrations) {
const result = await migration();
yield result;
}
}
To o czym warto wspomnieć przy tym kodzie:
function* () {...}
- to składnia funkcji generatora, wyróżnia go*
po słowiefunction
, która zwraca nam obiekt generatora, posiadający m.in. asynchroniczny iterator, funkcjęnext()
i wartość podczas danej iteracji- Słowo kluczowe
yield
poniekąd zastępuje return, ale różni się tym, że można go użyć wielokrotnie. Ustawia wartość obiektu generatora dla kolejnego wywołania metodynext()
- Użyta została także składania
async/await
pozwalająca "zaczekać" na wykonanie asynchronicznych operacji wewnątrz funkcji
Użycie generatora
const executeAllMigrations = async () => {
try {
for await (const result of runMigrations()) {
console.log(result);
}
console.log('Done all migrations')
} catch (error) {
console.log(error);
}
}
- Składania
try/catch
pozwala złapać błędy podczas wykonywania poszczególnych migracji - Pętla
for-await
iteruje po obiekcie generatora, za pomocą asynchronicznego iteratora
Uruchomienie migracji
executeAllMigrations();
Efekt?
Widzimy, że jeden ze skryptów migracji nie działa poprawnie i cały proces został zatrzymany zgodnie z oczekiwaniami - nie ujrzeliśmy w konsoli Done all migrations
.
Naprawmy to i sprawdźmy czy zadziała.
const migrations = [
() => createMigration('initial'),
() => createMigration('addTable'),
() => createMigration('addSomeField'),
() => createMigration('removeSomeField'),
];
Jak widać poniżej wszystko wykonało się zgodnie z planem.
To, na co chciałbym jeszcze zwrócić uwagę, to dokładny czas logów. Ustaliliśmy timeout na sekundę i w takim odstępie czasu wykonują się nasze operacje jedna po drugiej.
Wsparcie
Generatory
są częścią specyfikacji ES2015- Składania
async/await
dostępna jest od wersji ES2017 - w wersjach Node < 10 wymagane jest wykonanie skryptu z flagą
--harmony-async-iteration
Podsumowanie
Używanie generatorów jest czasami bardzo przydatne, a jak widać wcale nie tak skomplikowane jakby się mogło wydawać.
Można ich użyć tak, jak w tym wpisie, do wykonywania asynchronicznych operacji jedna po drugiej albo np. do przetwarzania jakiś podzielonych danych, które są stronicowane.
Warto wiedzieć jak działają i znać ich możliwości.
Autorem tekstu jest Piotr Sołtysiak.
Zdjęcie: Irvan Smith na Unsplash.