Înțelegerea generatoarelor în JavaScript

Autorul a selectat Open Internet/Free Speech Fund pentru a primi o donație ca parte a programului Write for DOnations.

Introducere

În ECMAScript 2015, generatoarele au fost introduse în limbajul JavaScript. Un generator este un proces care poate fi întrerupt și reluat și care poate produce mai multe valori. Un generator în JavaScript constă într-o funcție de generator, care returnează un obiect iterabil Generator.

Generatoarele pot menține starea, oferind o modalitate eficientă de a realiza iteratori, și sunt capabile să trateze fluxuri infinite de date, care pot fi utilizate pentru a implementa defilarea infinită în frontend-ul unei aplicații web, pentru a opera cu date de unde sonore și altele. În plus, atunci când sunt utilizate cu Promises, generatoarele pot imita funcționalitatea async/await, ceea ce ne permite să tratăm codul asincron într-un mod mai direct și mai ușor de citit. Deși async/await este o modalitate mai răspândită de a trata cazuri de utilizare asincronă comune și simple, cum ar fi preluarea de date de la o API, generatoarele au caracteristici mai avansate care fac ca învățarea modului de utilizare a acestora să merite.

În acest articol, vom acoperi modul de creare a funcțiilor generatoare, modul de iterație asupra obiectelor Generator, diferența dintre yield și return în interiorul unui generator, precum și alte aspecte ale lucrului cu generatoarele.

Funcții generatoare

O funcție generatoare este o funcție care returnează un obiect Generator și este definită prin cuvântul cheie function urmat de un asterisc (*), așa cum se arată în cele ce urmează:

// Generator function declarationfunction* generatorFunction() {}

Ocazional, veți vedea asteriscul alături de numele funcției, spre deosebire de cuvântul cheie al funcției, cum ar fi function *generatorFunction(). Aceasta funcționează la fel, dar function* este o sintaxă mai larg acceptată.

Funcțiile generatoare pot fi, de asemenea, definite într-o expresie, ca și funcțiile regulate:

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

Generatoarele pot fi chiar metodele unui obiect sau ale unei clase:

Exemplele din acest articol vor folosi sintaxa de declarare a funcțiilor generatoare.

Rețineți: Spre deosebire de funcțiile obișnuite, generatoarele nu pot fi construite cu ajutorul cuvântului cheie new și nici nu pot fi utilizate împreună cu funcțiile săgeată.

Acum că știți cum să declarați funcțiile generatoare, haideți să ne uităm la obiectele iterabile Generator pe care acestea le returnează.

Obiecte generatoare

În mod tradițional, funcțiile din JavaScript se execută până la finalizare, iar apelarea unei funcții va returna o valoare atunci când ajunge la cuvântul cheie return. În cazul în care cuvântul cheie return este omis, o funcție va returna implicit undefined.

În următorul cod, de exemplu, declarăm o funcție sum() care returnează o valoare care este suma a două argumente întregi:

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

Invocarea funcției returnează o valoare care este suma argumentelor:

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

O funcție generatoare, totuși, nu returnează imediat o valoare, și în schimb returnează un obiect iterabil Generator. În exemplul următor, declarăm o funcție și îi dăm o singură valoare de retur, ca o funcție standard:

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

Când invocăm funcția generatoare, aceasta va returna obiectul Generator, pe care îl putem atribui unei variabile:

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

Dacă aceasta ar fi fost o funcție obișnuită, ne-am fi așteptat ca generator să ne dea șirul returnat în funcție. Cu toate acestea, ceea ce obținem de fapt este un obiect într-o stare suspended. Prin urmare, apelarea lui generator va da un rezultat similar cu următorul:

Obiectul Generator returnat de funcție este un iterator. Un iterator este un obiect care are disponibilă o metodă next(), care este utilizată pentru a parcurge o secvență de valori. Metoda next() returnează un obiect cu proprietățile value și done. value reprezintă valoarea returnată, iar done indică dacă iteratorul a parcurs sau nu toate valorile sale.

Cunoscând acest lucru, să apelăm next() pe generator al nostru și să obținem valoarea curentă și starea iteratorului:

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

Aceasta va da următoarea ieșire:

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

Valoarea returnată în urma apelării next() este Hello, Generator!, iar starea lui done este true, deoarece această valoare provine de la un return care a închis iteratorul. Deoarece iteratorul este terminat, starea funcției generatoare se va schimba de la suspended la closed. Apelând din nou generator se vor obține următoarele:

Output
generatorFunction {<closed>}

Deocamdată, am demonstrat doar modul în care o funcție generatoare poate fi o modalitate mai complexă de a obține valoarea return a unei funcții. Dar funcțiile generatoare au, de asemenea, caracteristici unice care le deosebesc de funcțiile normale. În secțiunea următoare, vom învăța despre operatorul yield și vom vedea cum un generator poate întrerupe și relua execuția.

Operatori de randament

Generatoarele introduc un nou cuvânt cheie în JavaScript: yield. yield poate întrerupe o funcție generatoare și returnează valoarea care urmează yield, oferind o modalitate ușoară de a itera prin valori.

În acest exemplu, vom întrerupe funcția generatoare de trei ori cu valori diferite și vom returna o valoare la sfârșit. Apoi vom atribui obiectul nostru Generator la variabila generator.

Acum, când apelăm next() pe funcția generatoare, aceasta se va opri de fiecare dată când întâlnește yield. done va fi setată la false după fiecare yield, indicând că generatorul nu a terminat. Odată ce întâlnește un return, sau nu mai există alte yield întâlnite în funcție, done va trece la true, iar generatorul va fi terminat.

Utilizați metoda next() de patru ori la rând:

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

Acestea vor da următoarele patru linii de ieșire în ordine:

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

Rețineți că un generator nu are nevoie de un return; dacă este omis, ultima iterație va returna {value: undefined, done: true}, la fel ca și orice apeluri ulterioare la next() după ce un generator s-a terminat.

Iterarea peste un generator

Utilizând metoda next(), am iterat manual prin obiectul Generator, primind toate proprietățile value și done ale obiectului complet. Cu toate acestea, la fel ca Array, Map și Set, un Generator urmează protocolul de iterație și poate fi parcurs cu for...of:

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

Aceasta va returna următoarele:

Output
NeoMorpheusTrinity

Operatorul de împrăștiere poate fi folosit, de asemenea, pentru a atribui valorile unui Generator unui array.

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

Aceasta va da următoarea matrice:

Output
(3)

Atât spread cât și for...of nu vor factoriza return în valori (în acest caz, ar fi fost 'The Oracle').

Nota: În timp ce ambele metode sunt eficiente pentru a lucra cu generatoare finite, dacă un generator are de-a face cu un flux de date infinit, nu va fi posibil să se folosească direct spread sau for...of fără a crea o buclă infinită.

Închiderea unui generator

După cum am văzut, un generator poate avea proprietatea done setată la true și starea sa setată la closed prin iterarea prin toate valorile sale. Există două modalități suplimentare de a anula imediat un generator: cu metoda return() și cu metoda throw().

Cu return(), generatorul poate fi terminat în orice moment, la fel ca și când în corpul funcției s-ar fi aflat o instrucțiune return. Puteți trece un argument în return(), sau îl puteți lăsa gol pentru o valoare nedefinită.

Pentru a demonstra return(), vom crea un generator cu câteva valori yield, dar fără return în definiția funcției:

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

Primul next() ne va da 'Neo', cu done setat la false. Dacă invocăm o metodă return() pe obiectul Generator imediat după aceea, vom obține acum valoarea transmisă și done setată la true. Orice apel suplimentar la next() va da răspunsul generatorului completat implicit cu o valoare nedefinită.

Pentru a demonstra acest lucru, executați următoarele trei metode pe generator:

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

Aceasta va da următoarele trei rezultate:

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

Metoda return() a forțat obiectul Generator să se completeze și să ignore orice alte cuvinte cheie yield. Acest lucru este deosebit de util în programarea asincronă, atunci când trebuie să faceți ca funcțiile să poată fi anulate, cum ar fi întreruperea unei cereri web atunci când un utilizator dorește să efectueze o acțiune diferită, deoarece nu este posibilă anularea directă a unei Promise.

Dacă corpul unei funcții generatoare are o modalitate de a prinde și de a trata erorile, puteți utiliza metoda throw() pentru a arunca o eroare în generator. Aceasta pornește generatorul, aruncă eroarea înăuntru și termină generatorul.

Pentru a demonstra acest lucru, vom pune o try...catch în interiorul corpului funcției generatorului și vom înregistra o eroare dacă se găsește una:

Acum, vom rula metoda next(), urmată de throw():

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

Aceasta va da următoarea ieșire:

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

Utilizând throw(), am injectat o eroare în generator, care a fost prinsă de try...catch și înregistrată în consolă.

Metode și stări ale obiectului generator

Tabelul următor prezintă o listă de metode care pot fi utilizate pe obiectele Generator:

Metodă Descriere
next() Întoarce următoarea valoare într-un generator
return() Întoarce o valoare în un generator și termină generatorul
throw() Aruncă o eroare și termină generatorul

Tabelul următor enumeră stările posibile ale unui obiect Generator:

Status Descriere suspended Generatorul a oprit execuția, dar nu s-a terminat closed Generatorul s-a terminat fie prin întâlnirea unei erori, revenind, fie iterând prin toate valorile

yield Delegare

În plus față de operatorul obișnuit yield, generatoarele pot utiliza, de asemenea, expresia yield* pentru a delega alte valori unui alt generator. Atunci când yield* este întâlnit în cadrul unui generator, acesta va intra în generatorul delegat și va începe să parcurgă toate yield-urile până când generatorul respectiv este închis. Acest lucru poate fi folosit pentru a separa diferite funcții de generator pentru a vă organiza semantic codul, în timp ce toate yield-urile lor pot fi iterate în ordinea corectă.

Pentru a demonstra, putem crea două funcții generatoare, dintre care una va yield* opera asupra celeilalte:

În continuare, să iterăm prin funcția generatoare begin():

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

Aceasta va da următoarele valori în ordinea în care sunt generate:

Output
1234

Generatorul exterior a dat valorile 1 și 2, apoi a delegat la celălalt generator cu yield*, care a returnat 3 și 4.

yield* poate delega, de asemenea, către orice obiect iterabil, cum ar fi un Array sau o Mapă. Delegarea randamentului poate fi utilă în organizarea codului, deoarece orice funcție din cadrul unui generator care ar dori să utilizeze yield ar trebui să fie, de asemenea, un generator.

Curte de date infinite

Unul dintre aspectele utile ale generatoarelor este capacitatea de a lucra cu fluxuri de date și colecții infinite. Acest lucru poate fi demonstrat prin crearea unei bucle infinite în interiorul unei funcții de generator care incrementează un număr cu unu.

În următorul bloc de cod, definim această funcție generatoare și apoi inițiem generatorul:

Acum, iterați valorile folosind next():

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

Aceasta va da următoarea ieșire:

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

Funcția returnează valori succesive în bucla infinită, în timp ce proprietatea done rămâne false, asigurându-se că nu se va termina.

Cu generatoarele, nu trebuie să vă faceți griji cu privire la crearea unei bucle infinite, deoarece puteți opri și relua execuția în voie. Cu toate acestea, tot trebuie să aveți grijă la modul în care apelați generatorul. Dacă folosiți spread sau for...of pe un flux de date infinit, veți itera în continuare peste o buclă infinită deodată, ceea ce va provoca blocarea mediului.

Pentru un exemplu mai complex de flux de date infinit, putem crea o funcție de generator Fibonacci. Secvența Fibonacci, care însumează continuu cele două valori anterioare, poate fi scrisă folosind o buclă infinită în cadrul unui generator, după cum urmează:

Pentru a testa acest lucru, putem trece în buclă un număr finit și imprima secvența Fibonacci pe consolă.

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

Aceasta va da următoarele:

Output
0112358132134

Capacitatea de a lucra cu seturi de date infinite este o parte din ceea ce face generatoarele atât de puternice. Acest lucru poate fi util pentru exemple cum ar fi implementarea derulării infinite în frontend-ul unei aplicații web.

Pasarea valorilor în generatoare

De-a lungul acestui articol, am folosit generatoarele ca iteratori și am dat valori în fiecare iterație. Pe lângă faptul că produc valori, generatoarele pot, de asemenea, să consume valori din next(). În acest caz, yield va conține o valoare.

Este important să rețineți că primul next() care este apelat nu va transmite o valoare, ci doar va porni generatorul. Pentru a demonstra acest lucru, putem să înregistrăm valoarea lui yield și să apelăm next() de câteva ori cu anumite valori.

Aceasta va da următoarea ieșire:

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

De asemenea, este posibil să semănăm generatorul cu o valoare inițială. În exemplul următor, vom face o buclă for și vom trece fiecare valoare în metoda next(), dar vom trece și un argument pentru funcția inițială:

Vom prelua valoarea din next() și vom da o nouă valoare la următoarea iterație, care este valoarea anterioară înmulțită cu zece. Acest lucru va da următoarele:

Output
010203040

O altă modalitate de a aborda pornirea unui generator este de a îngloba generatorul într-o funcție care va apela întotdeauna next() o dată înainte de a face orice altceva.

async/await with Generators

O funcție asincronă este un tip de funcție disponibilă în JavaScript ES6+ care face ca lucrul cu date asincrone să fie mai ușor de înțeles, făcându-le să pară sincrone. Generatoarele au o gamă mai extinsă de capacități decât funcțiile asincrone, dar sunt capabile să reproducă un comportament similar. Implementarea programării asincrone în acest mod poate crește flexibilitatea codului dumneavoastră.

În această secțiune, vom demonstra un exemplu de reproducere a async/await cu ajutorul generatoarelor.

Să construim o funcție asincronă care utilizează API Fetch pentru a obține date de la API JSONPlaceholder (care oferă date JSON de exemplu în scopuri de testare) și înregistrează răspunsul în consolă.

Începeți prin a defini o funcție asincronă numită getUsers care preia date de la API și returnează un array de obiecte, apoi apelați getUsers:

Aceasta va da date JSON similare cu următoarele:

Utilizând generatoare, putem crea ceva aproape identic care nu folosește cuvintele cheie async/await. În schimb, va folosi o nouă funcție pe care o creăm și valori yield în loc de promisiuni await.

În următorul bloc de cod, definim o funcție numită getUsers care folosește noua noastră funcție asyncAlt (pe care o vom scrie mai târziu) pentru a imita async/await.

După cum putem vedea, arată aproape identic cu implementarea async/await, cu excepția faptului că este trecută o funcție generatoare care produce valori.

Acum putem crea o funcție asyncAlt care seamănă cu o funcție asincronă. asyncAlt are ca parametru o funcție generatoare, care este funcția noastră care produce promisiunile pe care le returnează fetch. asyncAlt returnează ea însăși o funcție și rezolvă fiecare promisiune pe care o găsește până la ultima:

Aceasta va da același rezultat ca și versiunea async/await:

Rețineți că această implementare este pentru a demonstra modul în care generatoarele pot fi folosite în locul async/await și nu este un proiect gata de producție. Nu are configurată gestionarea erorilor și nici nu are capacitatea de a trece parametri în valorile generate. Deși această metodă poate adăuga flexibilitate codului dumneavoastră, de multe ori async/await va fi o alegere mai bună, deoarece abstractizează detaliile de implementare și vă permite să vă concentrați pe scrierea unui cod productiv.

Concluzie

Generatorii sunt procese care pot opri și relua execuția. Ele sunt o caracteristică puternică și versatilă a JavaScript, deși nu sunt utilizate în mod obișnuit. În acest tutorial, am învățat despre funcțiile generatoare și obiectele generatoare, metodele disponibile pentru generatoare, operatorii yield și yield*, precum și generatoarele utilizate cu seturi de date finite și infinite. Am explorat, de asemenea, o modalitate de a implementa cod asincron fără callback-uri imbricate sau lanțuri lungi de promisiuni.

Dacă doriți să învățați mai multe despre sintaxa JavaScript, aruncați o privire la tutorialele noastre Understanding This, Bind, Call, and Apply in JavaScript și Understanding Map and Set Objects in JavaScript.

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.