O scurtă introducere

Bine ați venit în minunata lume a funcțiilor de generator și a generatoarelor asincrone în JavaScript.

Sunteți pe cale să învățați despre una dintre cele mai exotice (în opinia majorității dezvoltatorilor) caracteristici ale JavaScript.

Să începem!

Funcții generatoare în JavaScript

Ce este o funcție generatoare?

O funcție generatoare (ECMAScript 2015) în JavaScript este un tip special de funcție sincronă care este capabilă să își oprească și să își reia execuția la voință.

În contrast cu funcțiile JavaScript obișnuite, care sunt de tip „trage și uită”, funcțiile generatoare au, de asemenea, capacitatea de a:

  • comunica cu apelantul pe un canal bidirecțional.
  • să-și păstreze contextul de execuție (domeniul de aplicare) pe parcursul apelurilor ulterioare.

Vă puteți gândi la funcțiile generatoare ca la niște închideri pe steroizi, dar asemănările se opresc aici!

Prima dvs. funcție generatoare

Pentru a crea o funcție generatoare punem o stea * după cuvântul cheie function:

function* generate() {//}

Rețineți: funcțiile generatoare pot lua, de asemenea, forma unei metode de clasă, sau a unei expresii de funcție. În schimb, funcțiile generatoare de săgeți nu sunt permise.

După ce ne aflăm în interiorul funcției putem folosi yield pentru a întrerupe execuția:

function* generate() { yield 33; yield 99;}

yield întrerupe execuția și returnează un așa-numit obiect Generator către apelant. Acest obiect este atât un iterabil, cât și un iterator în același timp.

Să demistificăm aceste concepte.

Iterabili și iteratori

Un obiect iterabil în JavaScript este un obiect care implementează Symbol.iterator. Iată un exemplu minim de iterabil:

const iterable = { : function() { /* TODO: iterator */ }};

După ce avem acest obiect iterabil, atribuim o funcție lui pentru a returna un obiect iterator.

Aceasta sună ca o mulțime de teorie, dar în practică un iterabil este un obiect asupra căruia putem face buclă cu for...of (ECMAScript 2015). Mai multe despre acest lucru într-un minut.

Ar trebui să știți deja câteva iterabile în JavaScript: array-urile și șirurile de caractere, de exemplu, sunt iterabile:

for (const char of "hello") { console.log(char);}

Alte iterabile cunoscute sunt Map și Set. for...of vine, de asemenea, la îndemână pentru iterația asupra valorilor unui obiect:


Amintiți-vă doar că orice proprietate marcată ca enumerable: false nu va apărea în iterație:

const person = { name: "Juliana", surname: "Crain", age: 32};Object.defineProperty(person, "city", { enumerable: false, value: "London"});for (const value of Object.values(person)) { console.log(value);}// Juliana// Crain// 32

Acum, problema cu iterabilul nostru personalizat este că nu poate merge departe singur fără un iterator.

Iteratorii sunt, de asemenea, obiecte, dar ar trebui să se conformeze protocolului iteratorilor. Pe scurt, iteratorii trebuie să aibă cel puțin o metodă next().

next() trebuie să returneze un alt obiect, ale cărui proprietăți sunt value și done.

Logica metodei noastre next() trebuie să se supună următoarelor reguli:

  • restituim un obiect cu done: false pentru a continua iterația.
  • restituim un obiect cu done: true pentru a opri iterația.

value în schimb, ar trebui să rețină rezultatul pe care dorim să-l producem pentru consumator.

Să extindem exemplul nostru prin adăugarea iteratorului:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Aici avem un iterabil, care implementează corect Symbol.iterator. De asemenea, avem un iterator, care returnează:

  • un obiect a cărui formă este { value: x, done: false} până când count ajunge la 3.
  • un obiect a cărui formă este { value: x, done: true} când count ajunge la 3.

Acest iterabil minim este gata să fie iterat cu for...of:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};for (const iterableElement of iterable) { console.log(iterableElement);}

Rezultatul va fi:

123

După ce veți cunoaște mai bine funcțiile generatoare, veți vedea că iteratorii sunt baza pentru obiectele generatoare.

Rețineți:

  • iterabilele sunt obiectele asupra cărora iterăm.
  • iteratorii sunt lucrurile care fac ca iterabilul să fie … „loopable” peste.

În final, care este rostul unui iterabil?

Acum avem o buclă standard, fiabilă for...of care funcționează practic pentru aproape orice structură de date, personalizată sau nativă, în JavaScript.

Pentru a utiliza for...of pe structura dvs. de date personalizată trebuie să:

  • implementați Symbol.iterator.
  • furnizați un obiect iterator.

Asta este!

Mai multe resurse:

  • Iteratorii vor itera de Jake Archibald.

Iterabilele sunt expandabile și destructurabile

În plus față de for...of, putem folosi, de asemenea, extinderea și destructurarea pe iterabile finite.

Considerați din nou exemplul anterior:

const iterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return { value: count, done: false }; } return { value: count, done: true }; } }; }};

Pentru a extrage toate valorile putem împrăștia iterabilul într-un array:

const values = ;console.log(values); // 

Pentru a extrage în schimb doar câteva valori putem destructura array-ul iterabilului. Aici obținem prima și a doua valoare din iterabil:

const = iterable;console.log(first); // 1console.log(second); // 2

Aici, în schimb, obținem prima și a treia valoare:

const = iterable;console.log(first); // 1console.log(third); // 3

Să ne îndreptăm din nou atenția asupra funcțiilor generatoare.

Extragerea datelor dintr-o funcție generatoare

După ce o funcție generatoare este în funcțiune, putem începe să interacționăm cu ea. Această interacțiune constă în:

  • obținerea de valori de la generator, pas cu pas.
  • opțional trimiterea de valori înapoi la generator.

Pentru a extrage valori dintr-un generator putem folosi trei abordări:

  • apelarea next() asupra obiectului iterator.
  • iterația cu for...of.
  • împrăștierea și destructurarea tablourilor.

O funcție generatoare nu calculează toate rezultatele sale într-un singur pas, așa cum fac funcțiile obișnuite.

Dacă luăm exemplul nostru, pentru a obține valori de la generator putem mai întâi de toate să încălzim generatorul:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();

Aici go devine obiectul nostru iterabil/iterator, rezultatul apelării generate.

(Rețineți, un obiect Generator este atât un iterabil cât și un iterator).

De acum înainte putem apela go.next() pentru a avansa execuția:

function* generate() { yield 33; yield 99;}const go = generate();// Consume the generatorconst { value: firstStep } = go.next(); // firstStep is 33const { value: secondStep } = go.next(); // secondStep is 99

Aici fiecare apel la go.next() produce un nou obiect. În exemplu, destructurăm proprietatea value din acest obiect.

Obiectele returnate în urma apelului next() asupra obiectului iterator au două proprietăți:

  • value: valoarea pentru pasul curent.
  • done: un boolean care indică dacă există sau nu mai multe valori în generator.

Am implementat un astfel de obiect iterator în secțiunea anterioară. Atunci când folosiți un generator, obiectul iterator este deja acolo pentru dumneavoastră.

next() funcționează bine pentru a extrage date finite dintr-un obiect iterator.

Pentru a itera peste date nedefinite, putem folosi for...of. Iată un generator infinit:

function* endlessGenerator() { let counter = 0; while (true) { counter++; yield counter; }}// Consume the generatorfor (const value of endlessGenerator()) { console.log(value);}

După cum puteți observa, nu este nevoie să inițializăm generatorul atunci când folosim for...of.

În cele din urmă, putem, de asemenea, să răspândim și să destructurăm obiectul generator. Iată împrăștierea:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // 

Iată destructurarea:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Destructuringconst = go;console.log(first); // 1console.log(second); // 2

Este important să rețineți că generatoarele se epuizează odată ce le consumați toate valorile.

Dacă împrăștiați valorile dintr-un generator, nu mai rămâne nimic de scos după aceea:

function* generate() { yield 33; yield 99;}// Initialize the generatorconst go = generate();// Spreadconst values = ;console.log(values); // // Exhaustconst = go;console.log(first); // undefinedconsole.log(second); // undefined

Generatoarele funcționează și în sens invers: ele pot accepta valori sau comenzi de la apelant, așa cum vom vedea într-un minut.

Revenind la funcțiile de generator

Obiectele Iterator, obiectul rezultat în urma apelului unei funcții de generator, expun următoarele metode:

  • next()
  • return()
  • throw()

Am văzut deja next(), care ajută la extragerea obiectelor dintr-un generator.

Utilizarea sa nu se limitează doar la extragerea de date, ci și la trimiterea de valori către generator.

Considerați următorul generator:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}

El listează un parametru string. Noi furnizăm acest argument la inițializare:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");

De îndată ce apelăm next() pentru prima dată pe obiectul iterator, execuția începe și produce „A”:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // A

În acest moment putem vorbi cu generatorul prin furnizarea unui argument pentru next:

go.next("b");

Iată lista completă:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // B

De acum încolo, putem introduce valori în yield de fiecare dată când avem nevoie de un nou șir de caractere majuscule:

function* endlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}const go = endlessUppercase("a");console.log(go.next().value); // Aconsole.log(go.next("b").value); // Bconsole.log(go.next("c").value); // Cconsole.log(go.next("d").value); // D

Dacă în orice moment dorim să ne întoarcem de tot din execuție, putem folosi return pe obiectul iterator:

const { value } = go.return("stop it");console.log(value); // stop it

Aceasta oprește execuția.

În plus față de valori, puteți, de asemenea, să aruncați o excepție în generator. Vedeți „Gestionarea erorilor pentru funcțiile generatoare”.

Cazuri de utilizare pentru funcțiile generatoare

Majoritatea dezvoltatorilor (inclusiv eu) văd funcțiile generatoare ca pe o caracteristică exotică a JavaScript care are puține sau nici o aplicație în lumea reală.

Acest lucru ar putea fi adevărat pentru munca medie de front-end, unde un strop de jQuery și un pic de CSS pot face treaba de cele mai multe ori.

În realitate, funcțiile generatoare strălucesc cu adevărat în toate acele scenarii în care performanțele sunt primordiale.

În special, ele sunt bune pentru:

  • lucrul cu fișiere și seturi de date de mari dimensiuni.
  • lucrul cu date în back-end, sau în front-end.
  • generarea de secvențe infinite de date.
  • computerea logicii costisitoare la cerere.

Funcțiile generatoare sunt, de asemenea, blocul de construcție pentru modele asincrone sofisticate cu funcții generatoare asincrone, subiectul nostru pentru secțiunea următoare.

Funcții generatoare asincrone în JavaScript

Ce este o funcție generatoare asincronă?

O funcție generatoare asincronă (ECMAScript 2018) este un tip special de funcție asincronă care este capabilă să își oprească și să își reia execuția la voință.

Diferența dintre funcțiile generatoare sincrone și funcțiile generatoare asincrone constă în faptul că acestea din urmă returnează un rezultat asincron, bazat pe promisiuni, de la obiectul iterator.

La fel ca și funcțiile generatoare, funcțiile generatoare asincrone sunt capabile să:

  • comunice cu apelantul.
  • își păstreze contextul de execuție (domeniul de aplicare) pe parcursul apelurilor ulterioare.

Prima dvs. funcție generatoare asincronă

Pentru a crea o funcție generatoare asincronă declarăm o funcție generatoare cu steaua *, prefixată cu async:

async function* asyncGenerator() { //}

După ce ne aflăm în interiorul funcției, putem folosi yield pentru a întrerupe execuția:

async function* asyncGenerator() { yield 33; yield 99;}

Aici yield întrerupe execuția și returnează un așa-numit obiect Generator către apelant.

Acest obiect este atât un iterabil, cât și un iterator în același timp.

Să recapitulăm aceste concepte pentru a vedea cum se potrivesc pe tărâmul asincron.

Iterabili și iteratori asincroni

Un iterabil asincron în JavaScript este un obiect care implementează Symbol.asyncIterator.

Iată un exemplu minimal:

const asyncIterable = { : function() { /* TODO: iterator */ }};

După ce avem acest obiect iterabil, atribuim o funcție lui pentru a returna un obiect iterator.

Obiectul iterator ar trebui să se conformeze protocolului iterator cu o metodă next() (ca și iteratorul sincron).

Să extindem exemplul nostru adăugând iteratorul:

const asyncIterable = { : function() { let count = 0; return { next() { count++; if (count <= 3) { return Promise.resolve({ value: count, done: false }); } return Promise.resolve({ value: count, done: true }); } }; }};

Acest iterator este similar cu cel pe care l-am construit în secțiunile anterioare, de data aceasta singura diferență este că înfășurăm obiectul care se întoarce cu Promise.resolve.

În acest moment putem face ceva de genul:

const go = asyncIterable();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));// 1// 2

Sau cu for await...of:

async function consumer() { for await (const asyncIterableElement of asyncIterable) { console.log(asyncIterableElement); }}consumer();// 1// 2// 3

Iterabilele asincrone și iteratorii asincroni sunt baza pentru funcțiile generatoare asincrone.

Să ne îndreptăm acum din nou atenția asupra lor.

Extragerea datelor dintr-un generator asincron

Funcțiile generatoare asincrone nu își calculează toate rezultatele într-un singur pas, așa cum fac funcțiile obișnuite.

În schimb, extragem valorile pas cu pas.

După ce am examinat iteratorii și iterabilele asincrone, nu ar trebui să fie o surpriză să vedem că pentru a extrage Promises dintr-un generator asincron putem folosi două abordări:

  • apelând next() pe obiectul iterator.
  • iterația asincronă cu for await...of.

În exemplul nostru inițial putem face:

async function* asyncGenerator() { yield 33; yield 99;}const go = asyncGenerator();go.next().then(iterator => console.log(iterator.value));go.next().then(iterator => console.log(iterator.value));

Scopul acestui cod este:

3399

Celealaltă abordare folosește iterația asincronă cu for await...of. Pentru a utiliza iterația asincronă, înfășurăm consumatorul cu o funcție async.

Iată exemplul complet:

async function* asyncGenerator() { yield 33; yield 99;}async function consumer() { for await (const value of asyncGenerator()) { console.log(value); }}consumer();

for await...of funcționează bine pentru extragerea fluxurilor de date nedefinite.

Să vedem acum cum trimitem datele înapoi la generator.

Vorbind din nou despre funcțiile generatorului asincron

Considerați următorul generator asincron:

async function* asyncGenerator(string) { while (true) { string = yield string.toUpperCase(); }}

La fel ca în cazul mașinii cu majuscule nesfârșite din exemplul cu generatorul, putem furniza un argument pentru next():

async function* asyncEndlessUppercase(string) { while (true) { string = yield string.toUpperCase(); }}async function consumer() { const go = await asyncEndlessUppercase("a"); const { value: firstStep } = await go.next(); console.log(firstStep); const { value: secondStep } = await go.next("b"); console.log(secondStep); const { value: thirdStep } = await go.next("c"); console.log(thirdStep);}consumer();

Aici fiecare pas trimite o nouă valoare în generator.

Lovitura acestui cod este:

ABC

Chiar dacă nu există nimic asincron în mod inerent în toUpperCase(), puteți simți scopul acestui model.

Dacă în orice moment dorim să ieșim din execuție, putem apela return() asupra obiectului iterator:

 const { value } = await go.return("stop it"); console.log(value); // stop it

În plus față de valori, puteți arunca o excepție în generator. Vedeți „Gestionarea erorilor pentru generatoarele asincrone”.

Cazuri de utilizare pentru iterabile asincrone și funcții generatoare asincrone

Dacă funcțiile generatoare sunt bune pentru a lucra sincron cu fișiere mari și secvențe infinite, funcțiile generatoare asincrone permit un nou tărâm de posibilități pentru JavaScript.

În special, iterația asincronă facilitează consumul de fluxuri lizibile. Obiectul Response din Fetch expune body ca un flux lizibil cu getReader(). Putem înfășura un astfel de flux cu un generator asincron și mai târziu să iterăm peste el cu for await...of.

Async iterators and generators de Jake Archibald are o mulțime de exemple frumoase.

Alte exemple de fluxuri sunt fluxurile de cereri cu Fetch.

În momentul scrierii acestui articol nu există niciun API de browser care să implementeze Symbol.asyncIterator, dar specificația Stream va schimba acest lucru.

În Node.js, recenta API Stream se joacă bine cu generatoarele asincrone și iterația asincronă.

În viitor vom putea consuma și lucra fără probleme cu fluxuri inscriptibile, citibile și transformatoare pe partea de client.

Resurse suplimentare:

  • The Stream API.

Încheiere

Termeni și concepte cheie pe care le-am acoperit în această postare:

ECMAScript 2015:

  • iterable.
  • iterator.

Acestea sunt blocurile de construcție pentru funcțiile generatoare.

ECMAScript 2018:

  • iterabil asincron.
  • iterator asincron.
  • iterator asincron.

În schimb, acestea sunt blocurile de construcție pentru funcțiile generatoare asincrone.

O bună înțelegere a iterabilelor și iteratorilor vă poate duce departe. Nu că veți lucra cu funcții generatoare și funcții generatoare asincrone în fiecare zi, dar acestea sunt o abilitate bună de avut în centura dumneavoastră de instrumente.

Și dumneavoastră? Le-ați folosit vreodată?

Mulțumesc pentru lectură și rămâneți pe acest blog!

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.