Polub bloga na fejsie!

Generatory ES6 – podstawy

8

W sumie to kiedyś już na blogu wyjaśniłem czym są i do czego służą generatory ES6. Było to przy okazji wpisu na temat redux-saga, która korzysta właśnie z tego, dość nowego w JavaScript, mechanizmu. Jednak od czasu tamtego posta, dostałem już kilka wiadomości z prośbą o opisanie generatorów w osobnym wpisie, postanowiłem więc dziś tę potrzebę spełnić. Jednak te maile od czytelników to fajna sprawa, bo nie miałem za bardzo pomysłu na dzisiejszy wpis…

Poniżej dowiesz się co nieco na temat generatorów ES6. Wszystko oczywiście poparte przykładami. Postaram się dużo bardziej wyczerpać temat niż to zrobiłem w poście o redux-saga. Myślę, że nie ma już co przedłużać – przejdźmy do rzeczy!

Czym są generatory ES6?

Chyba najbardziej kompletnym źródłem wiedzy na temat generatorów ES6 (i w ogóle ES6+) jest Axel Rauschmayer – tutaj jego blog oraz dostępna on-line książka o ES6. Moim zdaniem, bardzo fajnie opisuje on generatory jako procesy, których wykonanie można pauzować oraz wznawiać (za chwilę pokażę o co chodzi).

Z punktu widzenia składni, generatory ES6 są natomiast pewnym rodzajem funkcji. Do ich definiowania służy specjalny „keyword”: function* (funkcja z gwiazdką):

Powyżej mamy bardzo prostą funkcję generatora, której jedynym zadaniem jest wyświetlenie tekstu na ekranie. Funkcję generatora można normalnie wywoływać, a jej wynik najlepiej przypisać do zmiennej/stałej (nawet jeśli nie użyliśmy return):

Robimy tak dlatego, że wywołanie funkcji generatora nie powoduje wykonania ciała tej funkcji. Zamiast tego, wywołanie generatora zwraca obiekt tak zwanego iteratora. Wspomniałem wcześniej, że generatory możemy traktować jak procesy… obiekt iteratora służy do kontrolowania tego procesu, a konkretniej umożliwia nam wznawianie go. Do tego celu wykorzystujemy metodę next() (poniżej cały kod przykładu):

Przeanalizujmy teraz całość. Najpierw utworzony został generator, następnie wywołano go, w celu pobrania obiektu iteratora. Na końcu wywołano metodę next() iteratora, co spowodowało wykonanie ciała funkcji generatora.

Korzystając z analogii, którą zaproponował Rauschmayer: utworzono proces, który na początku jest „spauzowany”, a następnie go wznowiono (za pomocą wywołania next()) i wykonano w całości. Ok, ale co z pauzowaniem?

Polecenie yield

Jeśli metoda next() wznawia wykonanie ciała funkcji generatora, to polecenie yield je wstrzymuje. Zresztą spójrz na modyfikację powyższego przykładu:

Najpierw zwróć uwagę na zmiany w funkcji generatora: mamy teraz dwa wywołania console.log() przedzielone wywołaniem polecania yield. Spójrz teraz na koniec przykładu: mamy tutaj dwa wywołania metody next().  Jak widzisz, pierwsze z nich wykonuje tylko kod od początku funkcji generatora, do wywołania polecenia yield. Drugie natomiast wykonuje resztę ciała funkcji generatora.

Polecenie yield może też zwracać wartość:

Efekt powyższego kodu będzie następujący:

wynik działania powyższego kodu

Jak widzisz, działa to tak: pierwsza metoda next() wykonuje kod od początku do wywołania yield. Następnie polecenie yield zwraca wartość „test”, która jest pakowana do właściwości value specjalnego obiektu, który zwracany jest przez pierwszą metodę next(). Kolejne wywołanie next() wykonuje resztę kodu generatora i również zwraca taki obiekt. Tym razem wartość właściwości value tego obiektu to „undefined”.

POLUB BLOGA NA FACEBOOKU!

Chcesz być na bieżąco informowany o nowościach na blogu oraz innych ciekawych treściach? Polub fanpage bloga na Facebooku!

Zwróć też uwagę, że obiekty zwracane przez metodę next(), oprócz właściwości value posiadają też właściwość done. Informuje ona, czy jest coś jeszcze do zrobienia: jeśli wartość właściwości done równa jest false, oznacza to, że można jeszcze wywołać kolejne next().

Polecenie return

Wspomniałem przed chwilą, że obiekt zwracany przez ostatnie wywołanie metody next() zawiera właściwość value o wartości „undefinded”. To się jednak zmieni, jeśli na funkcja generatora będzie zwracała wartość za pomocą polecania return:

Potwierdza to podgląd konsoli przeglądarki po wykonaniu powyższego kodu:

podgląd konsoli przeglądarki

Pętle

Wspomniałem wcześniej, że obiekt zwracany przy wywołaniu funkcji generatora to iterator. Nazwa jest nieprzypadkowa, ponieważ obiekt ten może być wykorzystany w pętli (mówimy, że jest „iterowalny”) czyli można go użyć w konstrukcji for ... of (link):

Powyższa implementacja funkcji generatora zawiera pętlę, której każdy przebieg wywołuje polecenie yield. Dla prostoty przykładu, zwracana jest po prostu wartość indeksu iteracji.

Spójrz teraz na koniec przykładu. Jak wspomniałem, obiektu iteratora można użyć w pętli for ... of. Działa to w ten sposób, że każdy przebieg tej pętli wywołuje pod spodem metodę next() iteratora, która z kolei powoduje wykonanie jednego przebiegu pętli zaimplementowanej w funkcji generatora. Każdy przebieg tej pętli kończy się na wywołaniu polecenia yield. A że polecenie to zwraca wartość, to jest ona przypisywana do właściwości value obiektu zwracanego przez next(). Wartość tej właściwości jest następnie, pod spodem, wyciągana i przypisywana do zmiennej iteracji (w przykładzie stała item).

W ten sposób w konsoli uzyskujemy ciąg liczb od 0 do 9.

Generator jako „obserwator”

Obiekt zwracany przez generator, oprócz tego, że jest „iterowalny” może też być „obserwowalny”. Polecenie yield potrafi bowiem również przyjmować wartości przekazane do generatora jako parametr wywołania metody next(). Zresztą najlepiej będzie jak przeanalizujemy przykład:

Tym razem przeanalizujmy przykład trochę od końca. Jak widzisz, mamy tutaj trzy wywołania metody next(). Pierwsze z nich opisane jest jako to, które uruchamia „obserwatora”. Działa to bowiem tak: pierwsze wywołanie next() wykonuje kod funkcji generatora od początku aż do pierwszego wystąpienia polecenia yield i na tym kończy. Dopiero drugie wywołanie next() powoduje wykonanie kodu od tego miejsca, aż do kolejnego polecenia yield itd.

W naszym przykładzie zadziała więc to tak: pierwsze next() uruchamia pętlę, ale już nie wykonuje kodu od linii trzeciej włącznie. Nawet jeśli przekazalibyśmy do generatora jakąś wartość poprzez parametr metody next() to został by on zignorowany. Kolejne wywołanie next(), tym razem z parametrem, powoduje przekazanie wartości tego parametru do generatora poprzez yield. Wartość ta jest następnie, w trzeciej linii przykładu, przypisywana do stałej val, a kod wykonuje się dalej, wyświetlając tę wartość na konsoli. Następnie rozpoczyna się kolejna iteracja pętli, która znów zatrzymuje się na poleceniu yield (ale linia ta już się nie wykonuje). Kolejne wywołanie next() znów powoduje przypisanie wartości parametru i wykonanie reszty kodu, itd.

Powyższe pokazuje, że polecenie yield może też służyć do przyjmowania wartości. Zresztą za chwilę nam się to przyda…

Dodatek: sposoby deklarowania generatorów

W powyższych przykładach stosowałem zwykłą deklarację funkcji generatora: function* generator() { ... }. Oczywiście, tak jak i przy normalnych funkcjach istnieją też inne sposoby deklaracji generatorów.

Na pierwszy ogień idzie wyrażenie funkcyjne (ang. „function expression”):

Tutaj raczej nie ma czego wyjaśniać, gwiazdka pojawia się w tym samym miejscu co w przypadku zwykłej deklaracji funkcji.

Generator może też być jedną z metod literału obiektu. W takim przypadku jego deklaracja wygląda tak:

Oczywiście zamiast powyższego można stosować też, wspomniane wcześniej wyrażenie funkcyjne:

Jeśli generator ma być metodą klasy, to deklarujemy go podobnie jak w przypadku pierwszego przykładu deklaracji w literale obiektu:

Myślę, że warto znać powyższe sposoby deklaracji generatorów szczególnie, że ten w przypadku klasy i literału obiektu może nie być w pierwszej chwili oczywisty.

Przypadek użycia

To co przedstawiłem powyżej, to w zasadzie wszystko co trzeba wiedzieć o generatorach, aby z powodzeniem ich używać. Myślę jednak, że warto by było pokazać jeszcze jakiś rzeczywisty przypadek ich użycia w realnej sytuacji.

Najbardziej oczywistą sytuacją wydaje się wykorzystanie generatorów przy pobieraniu danych asynchronicznych. Dzięki nim (oraz na przykład bibliotece co) możemy pracować z danymi asynchronicznymi tak jakby były to dane synchroniczne:

Objaśnienie przykładu

W powyższym przykładzie widać dwie (asynchroniczne) funkcje zwracające obiekt Promise: są to getUsers() oraz getItems(). Następnie mamy generator, w którym wywołujemy powyższe metody, poprzedzone poleceniem yield. Wynik działania tych operacji przypisywany jest do stałych. Istotne jest tutaj to, że wywołanie drugiej funkcji zależy od wyniku działania pierwszej z nich. Na końcu widać wykorzystanie funkcji co, do której przekazujemy zdefiniowany przed chwilą generator.

Ogólnie działa to tak, że funkcja co wywołuje pod spodem generator, tworząc obiekt „obserwatora”. Następnie wywołuje odpowiednią ilość razy metodę next() iteratora, każdorazowo czekając na wynik danej operacji asynchronicznej. Gdy metoda next() zostanie wywołana po raz ostatni, zwraca całość w postaci jednego „promisa”, który możemy już wykorzystać w normalny sposób.

Jak widzisz, kod w którym korzystamy z metod asynchronicznych (w generatorze) wygląda na synchroniczny. Wywołując metodę getItems() zachowujemy się tak, jakby dane users na pewno już były pobrane. Jest to jednak operacja, która może trochę potrwać. Na szczęście tutaj wszystko dzieje się pod spodem, a my nie musimy się martwić – po prostu piszemy kod, tak jakby wszystkie dane były już dostępne.

P.S. We wpisie Co to jest Redux-Saga opisałem inny przykład użycia generatorów w kontekście pobierania danych asynchronicznych. Jeśli korzystasz z Reduxa, to ta wiedza może być dla Ciebie przydatna!

Podsumowanie

I to tyle na dziś – mam nadzieję, że w miarę przystępnie udało mi się przedstawić czym są generatory ES6. Jeśli coś jest nie jasne to daj znać w komentarzach – postaramy się (ja lub inny czytelnicy) rozwiać wszelkie wątpliwości. Zachęcam też do zapoznania się z rozdziałem książki wspomnianego wcześniej Axela Rauschmayera, w którym generatory ES6 zostały rozłożone na czynniki pierwsze!

Do wpisu powstało też dedykowane repozytorium na GitHubie. Możesz tam empirycznie przetestować to wszystko, o czym dziś napisałem!

REACT, REDUX, REACT-ROUTER - KURSY ON-LINE

Chcesz od podstaw poznać tajniki React, Redux oraz react-router? Zapraszam do moich szkoleń on-line:

Przejdź do szkoleń

Uwaga! Obecnie trwa przedsprzedaż kursów - premiera 1 sierpnia 2017!

  • Warto wspomnieć o nowej składni async/await, której podwaliny dało właśnie wykorzystanie generatorów do obsługi danych asynchronicznych.

    • w sumie racja, a przecież na blogu był już nawet post o async/await – później zaktualizuje oba wpisy – dzięki!

  • Niestety, dla mnie generatory w JS w dalszym ciągu są czarną magią jeśli chodzi o praktyczne wykorzystanie. Łączenie generatorów + promise, to w moim mniemaniu duplikowanie funkcji promise.
    Ciągle szukam ciekawszych przykładów użycia.
    Niemniej jednak, fajny wpis 🙂

    • fakt – jak poniżej wspomniał @Comandeer:disqus jest jeszcze składnia async/await, która rozwiązuje ten sam problem operacji asynchronicznych jaki próbowano rozwiązać właśnie generatorami… efekt jest podobny, czyli kod wyglądający na synchroniczny, jednak jest to trochę bardziej dedykowane rozwiązanie
      z drugiej strony tam też mamy do czynienia tak na prawdę z Promise’ami więc moim zdaniem to nie jest coś od czego w tym momencie można uciec – możliwość korzystania z Promisów w sposób synchroniczny jest, moim zdaniem, niewątpliwą zaletą mimo, że może to sprawiać wrażenie niepotrzebnej duplikacji

  • ekarolak

    Super artykuł 🙂

    PS. Znalazłem drobny błąd: Pętle, przykład kodu, linia 7: const iterator = decorator();

    • dzięki – poprawione! co do błedu to przez cały czas kiedy pisałem wpis, wciąż mi wychodził ten decorator zamiast generatora… na koniec sprawdziłem całość i myślałem, że wszędzie poprawiłem 😉 widać nie byłem dość dokładny…

  • Jakub

    Uwazam , iz uzywanie arrow functions z ES6 jest glupota , ogolnie same wporwadzenie ich bylo dla mnie glupota. W tym przypadku jeszcze jestem w stanie ogarnac, ale moge ci podac przyklady w , ktorych nawet Ty przez te strzalki stracisz orientacje w kodzie , zwykle funkcje robia to samo tylko sa o wiele bardziej czytelniejsze , pisalo juz o tym wielu js dev , ze nadaja sie tylko jesli nie zajmuja wiecej niz jednego wiersza

    • Dźwiedziu

      A ja uważam, że arrow functions to świetne rozwiązanie wszędzie tam, gdzie trzeba podać funkcję jako argument, a nasza funkcja jest trywialna.

Google Analytics Alternative