Rajesh Babu

Follow

4 maja, 2018 – 9 min read

.

Jeśli jesteś programistą JavaScript przez ostatnie dwa do pięciu lat, na pewno natknąłeś się na posty mówiące o Generatorach i Iteratorach. Podczas gdy Generatory i Iteratory są nieodłącznie powiązane, Generatory wydają się nieco bardziej onieśmielające niż te drugie.

Generator Turbine

Iteratory są implementacją obiektów Iterable takich jak mapy, tablice czy ciągi znaków, która umożliwia nam iterowanie po nich za pomocą funkcji next(). Mają szeroki zakres zastosowań w Generatorach, Obserwatorach i Operatorach rozproszenia.

Polecam następujący link dla tych z was, którzy są nowi w iteratorach, Przewodnik po Iteratorach.

Aby sprawdzić, czy twój obiekt jest zgodny z protokołem iteracyjnym, zweryfikuj używając wbudowanego Symbol.iterator:

Generatory wprowadzone jako część ES6 nie przeszły żadnych zmian dla dalszych wydań JavaScript i są tutaj, aby pozostać dłużej. Mam na myśli naprawdę długo! Więc nie ma od tego ucieczki. Chociaż ES7 i ES8 mają kilka nowych aktualizacji, nie mają tej samej wielkości zmian, które ES6 miał z ES5, który wziął JavaScript na następny poziom, że tak powiem.

Pod koniec tego postu, jestem pewien, że będziesz miał solidne zrozumienie, jak działają Generatory funkcji. Jeśli jesteś profesjonalistą, proszę, pomóż mi poprawić treść, dodając swoje komentarze w odpowiedziach. Na wszelki wypadek, jeśli masz trudności z podążaniem za kodem, dodałem również wyjaśnienie dla większości kodu, aby pomóc ci lepiej zrozumieć.

Funkcje w JavaScript, jak wszyscy wiemy, „działają aż do zwrotu/końca”. Funkcje Generatora, z drugiej strony, „działają aż do yield/return/end”. W przeciwieństwie do normalnych funkcji, funkcje generatora po wywołaniu zwracają obiekt Generatora, który przechowuje całą Iterable Generatora, która może być iterowana przy użyciu metody next() lub for…of loop.

Każde wywołanie next() na generatorze wykonuje każdą linię kodu aż do następnego yield, które napotka i tymczasowo zawiesza jego wykonanie.

Syntaktycznie są one identyfikowane za pomocą *, albo funkcja* X, albo funkcja *X, – obie oznaczają to samo.

Po utworzeniu, wywołanie funkcji generatora zwraca obiekt generatora. Ten obiekt generatora musi być przypisany do zmiennej, aby można było śledzić kolejne metody next() wywoływane na nim samym. Jeśli generator nie jest przypisany do zmiennej, to zawsze będzie zwracał tylko do pierwszego wyrażenia yield na każdym next().

Funkcje generatora są zwykle budowane przy użyciu wyrażeń yield. Każde wyrażenie yield wewnątrz funkcji generatora jest punktem zatrzymania przed rozpoczęciem kolejnego cyklu wykonania. Każdy cykl wykonania jest wyzwalany za pomocą metody next() na generatorze.

Na każde wywołanie next(), wyrażenie yield zwraca swoją wartość w postaci obiektu zawierającego następujące parametry.

{ value: 10, done: false } // assuming that 10 is the value of yield

  • Wartość – to wszystko, co jest zapisane po prawej stronie słowa kluczowego yield, może to być wywołanie funkcji, obiekt lub praktycznie wszystko. Dla pustych yield wartością tą jest undefined.
  • Done – określa status generatora, czy może on być dalej wykonywany, czy nie. Jeżeli done zwraca wartość true, oznacza to, że funkcja zakończyła swoje działanie.

(Jeśli uważasz, że to trochę za dużo powiedziane, zobaczysz przykład poniżej…)

Podstawowa funkcja generatora

Uwaga: W powyższym przykładzie funkcja generatora udostępniona bezpośrednio bez wrappera zawsze wykonuje się tylko do pierwszego yield. Dlatego z definicji musisz przypisać Generator do zmiennej, aby poprawnie iterować nad nim.

Cykl życia funkcji Generator

Zanim przejdziemy dalej, spójrzmy szybko na schemat blokowy cyklu życia funkcji Generator:

Cykl życia funkcji Generator

Za każdym razem, gdy napotkany zostanie yield, funkcja Generator zwraca obiekt zawierający wartość napotkanego yield oraz status done. Podobnie, gdy napotkany jest zwrot, otrzymujemy wartość zwrotu i również status done jako true. Ilekroć status done jest zwracany jako true, oznacza to, że funkcja generatora zakończyła swoje działanie i nie jest możliwe dalsze wykonywanie yield.

Wszystko po pierwszym zwrocie jest ignorowane, w tym inne wyrażenia yield.

Czytaj dalej, aby lepiej zrozumieć schemat blokowy.

Przypisywanie yield do zmiennej

W poprzedniej próbce kodu widzieliśmy wprowadzenie do tworzenia podstawowego generatora z yield. I otrzymaliśmy oczekiwane wyjście. Teraz załóżmy, że w poniższym kodzie przypiszemy całe wyrażenie yield do zmiennej.

Przypisanie yield do zmiennej

Jaki jest wynik całego wyrażenia yield przekazanego do zmiennej ? Nic lub Niezdefiniowane …

Dlaczego ? Począwszy od drugiej next(), poprzedni yield jest zastępowany argumentami przekazywanymi w następnej funkcji. Ponieważ, nie przekazujemy niczego w następnej metodzie, zakładamy, że całe 'poprzednie wyrażenie yield’ jest niezdefiniowane.

Mając to na uwadze, przeskoczmy do następnej sekcji, aby zrozumieć więcej o przekazywaniu argumentów do metody next().

Przekazywanie argumentów do metody next()

W odniesieniu do schematu blokowego powyżej, porozmawiajmy o przekazywaniu argumentów do następnej funkcji. Jest to jedna z najtrudniejszych części implementacji całego generatora.

Rozważmy następujący fragment kodu, gdzie yield jest przypisany do zmiennej, ale tym razem przekazujemy wartość w metodzie next().

Spójrzmy na poniższy kod w konsoli. A zaraz po nim wyjaśnienie.

Przekazywanie argumentów do metody next()

Wyjaśnienie:

  1. Gdy wywołujemy pierwszą metodę next(20), wypisywana jest każda linia kodu aż do pierwszego yield. Ponieważ nie mamy żadnego wcześniejszego wyrażenia yield ta wartość 20 jest odrzucana. Na wyjściu otrzymujemy wartość yield jako i*10, czyli tutaj 100. Również stan wykonania zatrzymuje się na pierwszym yield, a const j nie jest jeszcze ustawione.
  2. Drugie wywołanie next(10), zastępuje całe pierwsze wyrażenie yield wartością 10, imagine yield (i * 10) = 10, które przechodzi do ustawienia wartości const j na 50 przed zwróceniem wartości drugiego yield. Wartość yield wynosi tutaj 2 * 50 / 4 = 25.
  3. Trzeci next(5), zastępuje całe drugie wyrażenie yield wartością 5, sprowadzając wartość k do 5. I dalej kontynuuje wykonywanie instrukcji return i zwraca (x + y + z) => (10 + 50 + 5) = 65 jako końcową wartość yield wraz z wykonanym true.

To może być trochę przytłaczające dla czytelników po raz pierwszy, ale poświęć dobre 5 minut na przeczytanie tego w kółko, aby całkowicie zrozumieć.

Przekazywanie Yield jako argumentu funkcji

Istnieje n-numer przypadków użycia wokół yield dotyczących tego, jak można go użyć wewnątrz generatora funkcji. Spójrzmy na poniższy kod dla jednego z takich ciekawych przypadków użycia yield, wraz z wyjaśnieniem.

Yield jako argument funkcji

Wyjaśnienie

  1. Pierwsze next() daje niezdefiniowaną wartość, ponieważ wyrażenie yield nie ma wartości.
  2. Drugie next() zwraca „I am usless”, wartość, która została przekazana. I przygotowuje argument do wywołania funkcji.
  3. Trzeci next(), wywołuje funkcję z niezdefiniowanym argumentem. Jak wspomniano powyżej, metoda next() wywołana bez żadnych argumentów w zasadzie oznacza, że całe poprzednie wyrażenie yield jest niezdefiniowane. Stąd, to drukuje undefined i kończy bieg.

Yield z wywołaniem funkcji

Oprócz zwracania wartości yield może również wywoływać funkcje i zwracać wartość lub drukować to samo. Spójrzmy na poniższy kod i zrozumiemy go lepiej.

Yield wywołanie funkcji

Kod powyżej zwraca wartość obj jako wartość yield. I kończy bieg przez ustawienie undefined na const user.

Yield z obietnicami

Yield z obietnicami podąża za tym samym podejściem, co wywołanie funkcji powyżej, zamiast zwracać wartość z funkcji, zwraca obietnicę, która może być dalej oceniana pod kątem sukcesu lub porażki. Spójrzmy na poniższy kod, aby zrozumieć, jak to działa.

Yield with promises

The apiCall zwraca obietnice jako wartość yield, po rozwiązaniu po 2 sekundach drukuje wartość, której potrzebujemy.

Yield*

Do tej pory przyglądaliśmy się przypadkom użycia wyrażenia yield, teraz przyjrzymy się innemu wyrażeniu zwanemu yield*. Yield*, gdy jest użyte wewnątrz funkcji generatora, deleguje inną funkcję generatora. Po prostu, synchronicznie kończy funkcję generatora w swoim wyrażeniu przed przejściem do następnej linii.

Spójrzmy na kod i wyjaśnienie poniżej, aby lepiej zrozumieć. Ten kod pochodzi z MDN web docs.

Podstawowe yield*

Wyjaśnienie

  1. Pierwsze wywołanie next() daje wartość 1.
  2. Drugie wywołanie next() jest jednak wyrażeniem yield*, co z natury oznacza, że zamierzamy wykonać inną funkcję generatora określoną w wyrażeniu yield* przed kontynuowaniem bieżącej funkcji generatora.
  3. W swoim umyśle możesz założyć, że powyższy kod zostanie zastąpiony tak, jak poniższy
function* g2() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

To spowoduje zakończenie działania generatora. Istnieje jednak jedna wyraźna zdolność yield*, o której należy pamiętać podczas używania return, w następnej sekcji.

Yield* z return

Yield* z return zachowuje się nieco inaczej niż zwykłe yield*. Kiedy yield* jest używane z deklaracją return, ocenia do tej wartości, co oznacza, że cała funkcja yield*() staje się równa wartości zwróconej z powiązanej funkcji generatora.

Spójrzmy na kod i wyjaśnienie poniżej, aby lepiej to zrozumieć.

Yield* z return

Wyjaśnienie

  1. W pierwszym next() przechodzimy od razu do yield 1 i zwracamy jego wartość.
  2. Drugie next() zwraca 2.
  3. Trzecie next(), zwraca 'foo’ i przechodzi do yield 'the end’, przypisując po drodze 'foo’ do wyniku const.
  4. Ostatnia next() kończy działanie.

Yield* z wbudowanym obiektem iterowalnym

Jest jeszcze jedna interesująca właściwość yield* warta wspomnienia, podobna do wartości zwracanej, yield* może również iterować po obiektach iterowalnych takich jak Array, String i Map.

Przyjrzyjmy się jak to działa w czasie rzeczywistym.

Yield over built-in iterables

Tutaj w kodzie yield* iteruje nad każdym możliwym obiektem iterowalnym, który jest przekazywany jako jego wyrażenie. Myślę, że sam kod nie wymaga wyjaśnień.

Najlepsze praktyki

Na dodatek do tego wszystkiego, każdy iterator/generator może być iterowany nad pętlą for…of. Podobnie do naszej metody next(), która jest wywoływana jawnie, pętla for…of wewnętrznie przechodzi do następnej iteracji bazując na słowie kluczowym yield. I iteruje tylko do ostatniego yield i nie przetwarza instrukcji return jak metoda next().

Możesz zweryfikować to samo w poniższym kodzie.

Yield z for…of

Końcowa wartość zwracana nie jest drukowana, ponieważ pętla for…of iteruje tylko do ostatniego yield. Tak więc, jest to najlepsza praktyka, aby uniknąć instrukcji return wewnątrz funkcji generatora, ponieważ wpłynęłoby to na możliwość ponownego użycia funkcji, gdy jest ona iterowana przez for…of.

Zakończenie

Mam nadzieję, że to pokrywa podstawowe przypadki użycia funkcji generatora i mam szczerą nadzieję, że dało lepsze zrozumienie jak działają generatory w JavaScript ES6 i wyżej. Jeśli podoba Ci się moja treść proszę zostaw 1, 2, 3 lub nawet 50 klapsów :).

Proszę śledzić mnie na moim koncie GitHub po więcej projektów JavaScript i Full-Stack:

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.