Understanding Generators in JavaScript

Autor wybrał Open Internet/Free Speech Fund, aby otrzymać darowiznę w ramach programu Write for DOnations.

Wprowadzenie

W ECMAScript 2015, generatory zostały wprowadzone do języka JavaScript. Generator to proces, który może być wstrzymywany i wznawiany oraz może dawać wiele wartości. Generator w JavaScript składa się z funkcji generatora, która zwraca iterowalny Generator obiekt.

Generatory mogą utrzymywać stan, zapewniając efektywny sposób tworzenia iteratorów, i są zdolne do radzenia sobie z nieskończonymi strumieniami danych, które mogą być używane do implementacji nieskończonego przewijania na frontendzie aplikacji internetowej, do operowania na danych fal dźwiękowych i nie tylko. Dodatkowo, gdy są używane z Promises, generatory mogą naśladować funkcjonalność async/await, co pozwala nam radzić sobie z asynchronicznym kodem w bardziej prosty i czytelny sposób. Chociaż async/await jest bardziej rozpowszechnionym sposobem radzenia sobie z typowymi, prostymi asynchronicznymi przypadkami użycia, takimi jak pobieranie danych z API, generatory mają bardziej zaawansowane funkcje, które sprawiają, że warto nauczyć się z nich korzystać.

W tym artykule omówimy, jak tworzyć funkcje generatorów, jak iterować nad obiektami Generator, różnicę między yield i return wewnątrz generatora oraz inne aspekty pracy z generatorami.

Funkcje generatora

Funkcja generatora jest funkcją, która zwraca obiekt Generator i jest zdefiniowana przez słowo kluczowe function, po którym następuje gwiazdka (*), jak pokazano poniżej:

// Generator function declarationfunction* generatorFunction() {}

Okazjonalnie można zobaczyć gwiazdkę obok nazwy funkcji, w przeciwieństwie do słowa kluczowego funkcji, takiego jak function *generatorFunction(). Działa to tak samo, ale function* jest szerzej akceptowaną składnią.

Funkcje generatora mogą być również zdefiniowane w wyrażeniu, jak funkcje regularne:

// Generator function expressionconst generatorFunction = function*() {}

Generatory mogą być nawet metodami obiektu lub klasy:

Przykłady w tym artykule będą używać składni deklaracji funkcji generatora.

Uwaga: W przeciwieństwie do zwykłych funkcji, generatory nie mogą być konstruowane za pomocą słowa kluczowego new, ani nie mogą być używane w połączeniu z funkcjami strzałkowymi.

Gdy już wiesz, jak deklarować funkcje generatorów, przyjrzyjmy się zwracanym przez nie obiektom iterowalnym Generator.

Obiekty generatorów

Tradycyjnie, funkcje w JavaScript działają do końca, a wywołanie funkcji zwróci wartość, gdy dotrze do słowa kluczowego return. Jeśli słowo kluczowe return zostanie pominięte, funkcja domyślnie zwróci undefined.

Na przykład w poniższym kodzie deklarujemy funkcję sum(), która zwraca wartość będącą sumą dwóch argumentów całkowitych:

// A regular function that sums two valuesfunction sum(a, b) { return a + b}

Wywołanie funkcji zwraca wartość będącą sumą argumentów:

const value = sum(5, 6) // 11

Funkcja generatora nie zwraca jednak wartości natychmiast, a zamiast tego zwraca obiekt iterowalny Generator. W poniższym przykładzie deklarujemy funkcję i dajemy jej pojedynczą wartość zwracaną, jak w przypadku standardowej funkcji:

// Declare a generator function with a single return valuefunction* generatorFunction() { return 'Hello, Generator!'}

Gdy wywołamy funkcję generatora, zwróci ona obiekt Generator, który możemy przypisać do zmiennej:

// Assign the Generator object to generatorconst generator = generatorFunction()

Gdyby to była zwykła funkcja, spodziewalibyśmy się, że generator da nam łańcuch zwrócony przez funkcję. Jednak to, co faktycznie otrzymujemy, to obiekt w stanie suspended. Wywołanie generator da zatem wynik podobny do poniższego:

Obiekt Generator zwrócony przez funkcję jest iteratorem. Iterator jest obiektem, który ma dostępną metodę next(), która służy do iteracji przez sekwencję wartości. Metoda next() zwraca obiekt z właściwościami value i done. value reprezentuje zwróconą wartość, a done wskazuje, czy iterator przeszedł przez wszystkie swoje wartości, czy nie.

Wiedząc o tym, wywołajmy next() na naszym generator i uzyskajmy bieżącą wartość i stan iteratora:

// Call the next method on the Generator objectgenerator.next()

To da następujące wyjście:

Output
{value: "Hello, Generator!", done: true}

Wartość zwrócona z wywołania next() to Hello, Generator!, a stan done to true, ponieważ ta wartość pochodzi z return, który zamknął iterator. Ponieważ iterator jest skończony, stan funkcji generatora zmieni się z suspended na closed. Ponowne wywołanie generator da następujące wyniki:

Output
generatorFunction {<closed>}

Jak na razie pokazaliśmy tylko, jak funkcja generatora może być bardziej złożonym sposobem na uzyskanie wartości return funkcji. Ale funkcje generatora mają również unikalne cechy, które odróżniają je od zwykłych funkcji. W następnej sekcji poznamy operator yield i zobaczymy, jak generator może wstrzymywać i wznawiać wykonywanie.

Operatory yield

Generatory wprowadzają do JavaScriptu nowe słowo kluczowe: yield. yield może wstrzymać funkcję generatora i zwrócić wartość, która następuje po yield, zapewniając lekki sposób iteracji przez wartości.

W tym przykładzie wstrzymamy funkcję generatora trzy razy z różnymi wartościami i zwrócimy wartość na końcu. Następnie przypiszemy nasz obiekt Generator do zmiennej generator.

Teraz, gdy wywołamy next() na funkcji generatora, będzie ona pauzować za każdym razem, gdy napotka yield. done zostanie ustawione na false po każdym yield, wskazując, że generator nie zakończył pracy. Gdy napotka return, lub nie ma więcej yield napotkanych w funkcji, done zmieni się na true, a generator zostanie zakończony.

Użyj metody next() cztery razy z rzędu:

// Call next four timesgenerator.next()generator.next()generator.next()generator.next()

Dadzą one następujące cztery wiersze danych wyjściowych w kolejności:

Output
{value: "Neo", done: false}{value: "Morpheus", done: false}{value: "Trinity", done: false}{value: "The Oracle", done: true}

Zauważ, że generator nie wymaga return; jeśli zostanie pominięty, ostatnia iteracja zwróci {value: undefined, done: true}, podobnie jak wszelkie kolejne wywołania metody next() po zakończeniu generatora.

Iterating Over a Generator

Używając metody next(), ręcznie iterowaliśmy przez obiekt Generator, otrzymując wszystkie value i done właściwości pełnego obiektu. Jednakże, podobnie jak Array, Map i Set, obiekt Generator podlega protokołowi iteracji i może być iterowany za pomocą metody for...of:

// Iterate over Generator objectfor (const value of generator) { console.log(value)}

To zwróci następujące wyniki:

Output
NeoMorpheusTrinity

Operator rozprzestrzeniania może być również użyty do przypisania wartości Generator do tablicy.

// Create an array from the values of a Generator objectconst values = console.log(values)

Da to następującą tablicę:

Output
(3)

Zarówno spread, jak i for...of nie wpłyną na wartości return (w tym przypadku byłoby to 'The Oracle').

Uwaga: Podczas gdy obie te metody są skuteczne w pracy ze skończonymi generatorami, jeśli generator ma do czynienia z nieskończonym strumieniem danych, nie będzie możliwe bezpośrednie użycie spread lub for...of bez tworzenia nieskończonej pętli.

Zamykanie generatora

Jak widzieliśmy, generator może mieć swoją właściwość done ustawioną na true i swój stan ustawiony na closed poprzez iterację przez wszystkie swoje wartości. Istnieją dwa dodatkowe sposoby natychmiastowego anulowania generatora: za pomocą metody return() i za pomocą metody throw().

Z pomocą return() generator może zostać zakończony w dowolnym punkcie, tak jak gdyby w ciele funkcji znajdowała się instrukcja return. Możesz przekazać argument do return() lub pozostawić go pustym dla niezdefiniowanej wartości.

Aby zademonstrować return(), stworzymy generator z kilkoma wartościami yield, ale bez return w definicji funkcji:

function* generatorFunction() { yield 'Neo' yield 'Morpheus' yield 'Trinity'}const generator = generatorFunction()

Pierwsze next() da nam 'Neo', z done ustawionym na false. Jeśli zaraz po tym wywołamy metodę return() na obiekcie Generator, otrzymamy teraz przekazaną wartość, a done ustawimy na true. Każde dodatkowe wywołanie metody next() da domyślną zakończoną odpowiedź generatora z niezdefiniowaną wartością.

Aby to zademonstrować, uruchom następujące trzy metody na generator:

generator.next()generator.return('There is no spoon!')generator.next()

To da trzy następujące wyniki:

Output
{value: "Neo", done: false}{value: "There is no spoon!", done: true}{value: undefined, done: true}

Metoda return() zmusiła obiekt Generator do zakończenia i zignorowania wszelkich innych słów kluczowych yield. Jest to szczególnie przydatne w programowaniu asynchronicznym, gdy trzeba uczynić funkcje możliwymi do anulowania, np. przerywając żądanie sieciowe, gdy użytkownik chce wykonać inną akcję, ponieważ nie jest możliwe bezpośrednie anulowanie obietnicy.

Jeśli ciało funkcji generatora ma sposób na wyłapywanie i radzenie sobie z błędami, można użyć metody throw() do wrzucenia błędu do generatora. To uruchamia generator, wrzuca błąd i kończy działanie generatora.

Aby to zademonstrować, umieścimy metodę try...catch wewnątrz ciała funkcji generatora i zarejestrujemy błąd, jeśli taki zostanie znaleziony:

Teraz uruchomimy metodę next(), a następnie throw():

generator.next()generator.throw(new Error('Agent Smith!'))

To da następujące dane wyjściowe:

Output
{value: "Neo", done: false}Error: Agent Smith!{value: undefined, done: true}

Używając metody throw(), wstrzyknęliśmy do generatora błąd, który został złapany przez metodę try...catch i zalogowany do konsoli.

Metody i stany obiektu generatora

Poniższa tabela przedstawia listę metod, które mogą być używane na obiektach Generator:

Metoda Opis
next() Zwraca następną wartość w generatorze
return() Zwraca wartość w… generatorze i kończy działanie generatora
throw() Wyrzuca błąd i kończy działanie generatora

Następna tabela przedstawia możliwe stany obiektu Generator:

Status Opis
suspended Generator wstrzymał wykonywanie, ale nie zakończył działania
closed Generator zakończył działanie albo napotykając błąd, powrót, lub iterację przez wszystkie wartości

yield Delegacja

Oprócz zwykłego operatora yield, generatory mogą również używać wyrażenia yield* do delegowania kolejnych wartości do innego generatora. Gdy wyrażenie yield* zostanie napotkane wewnątrz generatora, przejdzie ono do delegowanego generatora i rozpocznie iterację przez wszystkie yield, aż do zamknięcia tego generatora. Może to być użyte do oddzielenia różnych funkcji generatora, aby semantycznie zorganizować swój kod, a jednocześnie mieć wszystkie ich yields iterowalne w odpowiedniej kolejności.

Aby zademonstrować, możemy utworzyć dwie funkcje generatora, z których jedna będzie yield* operować na drugiej:

Następnie wykonajmy iterację przez funkcję begin() generatora:

// Iterate through the outer generatorconst generator = begin()for (const value of generator) { console.log(value)}

To da następujące wartości w kolejności ich generowania:

Output
1234

Zewnętrzny generator dał wartości 1 i 2, a następnie delegował do drugiego generatora z yield*, który zwrócił 3 i 4.

yield* może również delegować do dowolnego obiektu, który jest iterowalny, takiego jak Array lub Map. Delegacja Yield może być pomocna w organizowaniu kodu, ponieważ każda funkcja wewnątrz generatora, która chciała użyć yield, musiałaby również być generatorem.

Niekończące się strumienie danych

Jednym z użytecznych aspektów generatorów jest możliwość pracy z nieskończonymi strumieniami danych i kolekcjami. Można to zademonstrować, tworząc nieskończoną pętlę wewnątrz funkcji generatora, która inkrementuje liczbę o jeden.

W poniższym bloku kodu zdefiniujemy tę funkcję generatora, a następnie zainicjujemy generator:

Teraz wykonaj iterację po wartościach za pomocą next():

// Iterate through the valuescounter.next()counter.next()counter.next()counter.next()

Da to następujące dane wyjściowe:

Output
{value: 0, done: false}{value: 1, done: false}{value: 2, done: false}{value: 3, done: false}

Funkcja zwraca kolejne wartości w nieskończonej pętli, podczas gdy właściwość done pozostaje false, zapewniając, że nie zakończy pracy.

W przypadku generatorów nie trzeba się martwić o tworzenie nieskończonej pętli, ponieważ można dowolnie wstrzymywać i wznawiać wykonywanie. Jednak nadal musisz być ostrożny z tym, jak wywołujesz generator. Jeśli użyjesz spread lub for...of na nieskończonym strumieniu danych, nadal będziesz iterował nad nieskończoną pętlą naraz, co spowoduje awarię środowiska.

Aby uzyskać bardziej złożony przykład nieskończonego strumienia danych, możemy utworzyć funkcję generatora Fibonacciego. Sekwencja Fibonacciego, która w sposób ciągły dodaje do siebie dwie poprzednie wartości, może być napisana przy użyciu nieskończonej pętli wewnątrz generatora, jak poniżej:

Aby to przetestować, możemy zapętlić skończoną liczbę i wypisać sekwencję Fibonacciego na konsolę.

// Print the first 10 values of fibonacciconst fib = fibonacci()for (let i = 0; i < 10; i++) { console.log(fib.next().value)}

To da następujące wyniki:

Output
0112358132134

Możliwość pracy z nieskończonymi zestawami danych jest jedną z części tego, co czyni generatory tak potężnymi. Może to być przydatne w takich przykładach, jak implementacja nieskończonego przewijania we front-endzie aplikacji internetowej.

Przekazywanie wartości w generatorach

Przez cały ten artykuł, używaliśmy generatorów jako iteratorów i przekazywaliśmy wartości w każdej iteracji. Oprócz produkowania wartości, generatory mogą również konsumować wartości z next(). W tym przypadku yield będzie zawierać wartość.

Ważne jest, aby zauważyć, że pierwszy next(), który zostanie wywołany, nie przekaże wartości, a jedynie uruchomi generator. Aby to zademonstrować, możemy zarejestrować wartość yield i wywołać next() kilka razy z pewnymi wartościami.

W ten sposób otrzymamy następujące dane wyjściowe:

Output
100200{value: "The end", done: true}

Możliwe jest również zasianie generatora wartością początkową. W poniższym przykładzie wykonamy pętlę for i przekażemy każdą wartość do metody next(), ale przekażemy również argument do funkcji początkowej:

Pobierzemy wartość z next() i przekażemy nową wartość do następnej iteracji, która jest poprzednią wartością razy dziesięć. Da to następujące wyniki:

Output
010203040

Innym sposobem radzenia sobie z uruchamianiem generatora jest zawinięcie generatora w funkcję, która zawsze wywoła next() raz, zanim zrobi cokolwiek innego.

async/await z generatorami

Funkcja asynchroniczna jest typem funkcji dostępnym w ES6+ JavaScript, który sprawia, że praca z danymi asynchronicznymi jest łatwiejsza do zrozumienia poprzez nadanie jej wyglądu synchronicznej. Generatory mają szerszy wachlarz możliwości niż funkcje asynchroniczne, ale są w stanie odwzorować podobne zachowanie. Implementowanie programowania asynchronicznego w ten sposób może zwiększyć elastyczność Twojego kodu.

W tym rozdziale zademonstrujemy przykład powielania async/await za pomocą generatorów.

Zbudujmy funkcję asynchroniczną, która używa interfejsu API Fetch do pobierania danych z interfejsu API JSONPlaceholder (który dostarcza przykładowych danych JSON do celów testowych) i loguje odpowiedź do konsoli.

Zacznij od zdefiniowania funkcji asynchronicznej o nazwie getUsers, która pobiera dane z API i zwraca tablicę obiektów, a następnie wywołaj getUsers:

To da dane JSON podobne do poniższych:

Używając generatorów, możemy stworzyć coś niemal identycznego, co nie używa słów kluczowych async/await. Zamiast tego użyje nowej funkcji, którą stworzymy, oraz wartości yield zamiast obietnic await.

W poniższym bloku kodu definiujemy funkcję o nazwie getUsers, która używa naszej nowej funkcji asyncAlt (którą napiszemy później), aby naśladować async/await.

Jak widzimy, wygląda to niemal identycznie jak implementacja async/await, z wyjątkiem tego, że przekazywana jest funkcja generatora, która zwraca wartości.

Teraz możemy utworzyć funkcję asyncAlt, która przypomina funkcję asynchroniczną. asyncAlt ma jako parametr funkcję generatora, która jest naszą funkcją, która zwraca obietnice, które fetch zwraca. asyncAlt zwraca samą funkcję i rozwiązuje każdą obietnicę, którą znajdzie, aż do ostatniej:

To da takie samo wyjście, jak wersja async/await:

Zauważ, że ta implementacja służy do zademonstrowania, jak generatory mogą być używane zamiast async/await, i nie jest projektem gotowym do produkcji. Nie ma ona ustawionej obsługi błędów, ani możliwości przekazywania parametrów do wartości uzyskanych. Chociaż ta metoda może dodać elastyczność do twojego kodu, często async/await będzie lepszym wyborem, ponieważ abstrahuje szczegóły implementacji i pozwala skupić się na pisaniu wydajnego kodu.

Zakończenie

Generatory są procesami, które mogą wstrzymywać i wznawiać wykonywanie. Są potężną, wszechstronną cechą JavaScriptu, choć nie są powszechnie używane. W tym tutorialu, nauczyliśmy się o funkcjach generatorów i obiektach generatorów, metodach dostępnych dla generatorów, operatorach yield i yield* oraz generatorach używanych z nieskończonymi i skończonymi zbiorami danych. Zbadaliśmy również jeden ze sposobów implementacji asynchronicznego kodu bez zagnieżdżonych wywołań zwrotnych lub długich łańcuchów obietnic.

Jeśli chcesz dowiedzieć się więcej o składni JavaScript, spójrz na nasze tutoriale Understanding This, Bind, Call, and Apply in JavaScript oraz Understanding Map and Set Objects in JavaScript.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.