Una rapida introduzione

Benvenuti nel meraviglioso mondo delle funzioni generatore e dei generatori asincroni in JavaScript.

Stai per imparare una delle più esotiche (secondo la maggioranza degli sviluppatori) caratteristiche di JavaScript.

Iniziamo!

Funzioni generatrici in JavaScript

Cos’è una funzione generatrice?

Una funzione generatrice (ECMAScript 2015) in JavaScript è un tipo speciale di funzione sincrona che è in grado di fermare e riprendere la sua esecuzione a volontà.

Al contrario delle normali funzioni JavaScript, che sono fire and forget, le funzioni generatrici hanno anche la capacità di:

  • comunicare con il chiamante su un canale bidirezionale.
  • mantenere il loro contesto di esecuzione (scope) sulle chiamate successive.

Puoi pensare alle funzioni generatrici come a delle chiusure sotto steroidi, ma le somiglianze si fermano qui!

La tua prima funzione generatrice

Per creare una funzione generatrice mettiamo una stella * dopo la parola chiave function:

function* generate() {//}

Nota: le funzioni generatrici possono anche assumere la forma di un metodo di classe o di una espressione di funzione. Al contrario, le funzioni generatrici di frecce non sono permesse.

Una volta dentro la funzione possiamo usare yield per mettere in pausa l’esecuzione:

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

yield mette in pausa l’esecuzione e restituisce un cosiddetto oggetto Generator a chi lo chiama. Questo oggetto è sia un iterabile che un iteratore allo stesso tempo.

Demistifichiamo questi concetti.

Iterabili e iteratori

Un oggetto iterabile in JavaScript è un oggetto che implementa Symbol.iterator. Ecco un esempio minimo di iterabile:

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

Una volta che abbiamo questo oggetto iterabile, assegniamo una funzione a per restituire un oggetto iteratore.

Sembra un sacco di teoria, ma in pratica un iterabile è un oggetto su cui possiamo fare un loop con for...of (ECMAScript 2015). Dovreste già conoscere un paio di iterabili in JavaScript: gli array e le stringhe per esempio sono iterabili:

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

Altri iterabili conosciuti sono Map e Set. for...of è anche utile per iterare i valori di un oggetto:

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

Basta ricordare che qualsiasi proprietà segnata come enumerable: false non apparirà nell’iterazione:

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

Ora, il problema del nostro iterabile personalizzato è che non può andare lontano da solo senza un iteratore.

Anche gli iteratori sono oggetti, ma devono essere conformi al protocollo iteratore. In breve, gli iteratori devono avere almeno un metodo next().

next() deve restituire un altro oggetto, le cui proprietà sono value e done.

La logica del nostro metodo next() deve obbedire alle seguenti regole:

  • ritorniamo un oggetto con done: false per continuare l’iterazione.
  • ritorniamo un oggetto con done: true per fermare l’iterazione.

value invece, dovrebbe contenere il risultato che vogliamo produrre per il consumatore.

Espandiamo il nostro esempio aggiungendo l’iteratore:

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

Qui abbiamo un iterabile, che implementa correttamente Symbol.iterator. Abbiamo anche un iteratore, che restituisce:

  • un oggetto la cui forma è { value: x, done: false} finché count non raggiunge 3.
  • un oggetto la cui forma è { value: x, done: true} quando count raggiunge 3.

Questo iterabile minimo è pronto per essere iterato con 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);}

Il risultato sarà:

123

Quando conoscerete meglio le funzioni generatrici, vedrete che gli iteratori sono la base degli oggetti generatori.

Tenete a mente:

  • gli iterabili sono gli oggetti su cui iteriamo.
  • gli iteratori sono le cose che rendono l’iterabile … “

Alla fine, qual è lo scopo di un iterabile?

Ora abbiamo un ciclo standard, affidabile for...of che funziona praticamente per quasi tutte le strutture di dati, personalizzate o native, in JavaScript.

Per usare for...of sulla tua struttura dati personalizzata devi:

  • implementare Symbol.iterator.
  • fornire un oggetto iteratore.

Ecco!

Altre risorse:

  • Iteratori che iterano di Jake Archibald.

Gli iterabili sono diffondibili e destrutturabili

In aggiunta a for...of, possiamo anche usare la diffusione e la destrutturazione su iterabili finiti.

Consideriamo di nuovo l’esempio precedente:

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

Per estrarre tutti i valori possiamo diffondere l’iterabile in un array:

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

Per estrarre solo un paio di valori invece possiamo destrutturare l’iterabile in array. Qui otteniamo il primo e il secondo valore dall’iterabile:

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

Qui invece otteniamo il primo, e il terzo:

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

Rivolgiamo ora la nostra attenzione sulle funzioni generatrici.

Estrarre dati da una funzione generatrice

Una volta che una funzione generatrice è in posizione possiamo iniziare a interagire con essa. Questa interazione consiste nel:

  • ottenere valori dal generatore, passo dopo passo.
  • opzionalmente inviare valori al generatore.

Per estrarre valori da un generatore possiamo usare tre approcci:

  • chiamare next() sull’oggetto iteratore.
  • iterazione con for...of.
  • diffusione e destrutturazione dell’array.

Una funzione generatrice non calcola tutti i suoi risultati in un solo passo, come fanno le funzioni regolari.

Se prendiamo il nostro esempio, per ottenere valori dal generatore possiamo prima di tutto scaldare il generatore:

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

Qui go diventa il nostro oggetto iterable/iterator, il risultato della chiamata a generate.

(Ricordate, un oggetto Generator è sia un iterabile che un iteratore).

Da ora in poi possiamo chiamare go.next() per far avanzare l’esecuzione:

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

Qui ogni chiamata a go.next() produce un nuovo oggetto. Nell’esempio destrutturiamo la proprietà value da questo oggetto.

Gli oggetti restituiti dalla chiamata a next() sull’oggetto iteratore hanno due proprietà:

  • value: il valore per il passo corrente.
  • done: un booleano che indica se ci sono più valori nel generatore, o no.

Abbiamo implementato tale oggetto iteratore nella sezione precedente. Quando usate un generatore, l’oggetto iteratore è già lì per voi.

next() funziona bene per estrarre dati finiti da un oggetto iteratore.

Per iterare su dati non finiti, possiamo usare for...of. Ecco un generatore infinito:

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

Come potete notare non c’è bisogno di inizializzare il generatore quando si usa for...of.

Infine, possiamo anche diffondere e destrutturare l’oggetto generatore. Ecco la diffusione:

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

Ecco la destrutturazione:

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

È importante notare che i generatori si esauriscono una volta consumati tutti i loro valori.

Se si spargono i valori da un generatore, non rimane nulla da estrarre dopo:

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

I generatori funzionano anche al contrario: possono accettare valori o comandi dal chiamante, come vedremo tra un minuto.

Tornando alle funzioni generatore

Gli oggetti generatore, l’oggetto risultante da una chiamata alla funzione generatore, espongono i seguenti metodi:

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

Abbiamo già visto next(), che aiuta a tirare fuori oggetti da un generatore.

Il suo uso non si limita solo all’estrazione di dati, ma anche all’invio di valori al generatore.

Considerate il seguente generatore:

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

Esiste un parametro string. Forniamo questo argomento all’inizializzazione:

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

Appena chiamiamo next() per la prima volta sull’oggetto iteratore, l’esecuzione inizia e produce “A”:

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

A questo punto possiamo rispondere al generatore fornendo un argomento per next:

go.next("b");

Ecco l’elenco completo:

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

D’ora in poi, possiamo dare valori a yield ogni volta che abbiamo bisogno di una nuova stringa maiuscola:

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

Se in qualsiasi momento vogliamo tornare completamente dall’esecuzione, possiamo usare return sull’oggetto iteratore:

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

Questo ferma l’esecuzione.

Oltre ai valori potete anche lanciare un’eccezione nel generatore. Vedi “Gestione degli errori per le funzioni generatore”.

Casi d’uso per le funzioni generatore

La maggior parte degli sviluppatori (me compreso) vede le funzioni generatore come una caratteristica esotica di JavaScript che ha poca o nessuna applicazione nel mondo reale.

Questo potrebbe essere vero per il lavoro medio di front-end, dove una spruzzata di jQuery e un po’ di CSS possono fare il trucco la maggior parte delle volte.

In realtà, le funzioni generatore brillano davvero in tutti quegli scenari dove le prestazioni sono fondamentali.

In particolare, sono buone per:

  • lavorare con grandi file e insiemi di dati.
  • gestire i dati nel back-end, o nel front-end.
  • generare sequenze infinite di dati.
  • computare una logica costosa su richiesta.

Le funzioni generatrici sono anche il blocco di costruzione per sofisticati modelli asincroni con funzioni generatrici asincrone, il nostro argomento per la prossima sezione.

Funzioni generatrici asincrone in JavaScript

Cos’è una funzione generatrice asincrona?

Una funzione generatrice asincrona (ECMAScript 2018) è un tipo speciale di funzione asincrona che è in grado di fermare e riprendere la sua esecuzione a volontà.

La differenza tra le funzioni generatrici sincrone e le funzioni generatrici asincrone è che queste ultime restituiscono un risultato asincrono, basato su Promise, dall’oggetto iteratore.

Come le funzioni generatrici, le funzioni generatrici asincrone sono in grado di:

  • comunicare con il chiamante.
  • mantenere il loro contesto di esecuzione (scope) sulle chiamate successive.

La vostra prima funzione generatrice asincrona

Per creare una funzione generatrice asincrona dichiariamo una funzione generatrice con la stella *, preceduta da async:

async function* asyncGenerator() { //}

Una volta dentro la funzione possiamo usare yield per mettere in pausa l’esecuzione:

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

Qui yield mette in pausa l’esecuzione e restituisce un oggetto Generator a chi lo chiama.

Questo oggetto è sia un iterabile che un iteratore allo stesso tempo.

Riassumiamo questi concetti per vedere come si adattano al terreno asincrono.

Iterabili e iteratori asincroni

Un iterabile asincrono in JavaScript è un oggetto che implementa Symbol.asyncIterator.

Ecco un esempio minimo:

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

Una volta che abbiamo questo oggetto iterabile, assegniamo una funzione a per restituire un oggetto iteratore.

L’oggetto iteratore dovrebbe essere conforme al protocollo iteratore con un metodo next() (come l’iteratore sincrono).

Espandiamo il nostro esempio aggiungendo l’iteratore:

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 }); } }; }};

Questo iteratore è simile a quello che abbiamo costruito nelle sezioni precedenti, questa volta l’unica differenza è che avvolgiamo l’oggetto di ritorno con Promise.resolve.

A questo punto possiamo fare qualcosa di simile:

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

Oppure con for await...of:

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

Le iterabili e gli iteratori asincroni sono la base per le funzioni generatrici asincrone.

Rivolgiamo ora la nostra attenzione su di loro.

Estrarre dati da un generatore asincrono

Le funzioni generatrici asincrone non calcolano tutti i loro risultati in un solo passo, come fanno le funzioni regolari.

Invece, tiriamo fuori i valori passo dopo passo.

Dopo aver esaminato gli iteratori asincroni e gli iterabili, non dovrebbe essere una sorpresa vedere che per tirare fuori le Promesse da un generatore asincrono possiamo usare due approcci:

  • chiamando next() sull’oggetto iteratore.
  • iterazione async con for await...of.

Nel nostro esempio iniziale possiamo fare:

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));

L’output di questo codice è:

3399

L’altro approccio usa l’iterazione async con for await...of. Per usare l’iterazione async avvolgiamo il consumatore con una funzione async.

Ecco l’esempio completo:

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

for await...of funziona bene per estrarre flussi di dati non finiti.

Vediamo ora come inviare i dati al generatore.

Tornando alle funzioni asincrone del generatore

Consideriamo il seguente generatore asincrono:

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

Come la macchina maiuscola infinita dell’esempio del generatore, possiamo fornire un argomento a 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();

Qui ogni passo invia un nuovo valore nel generatore.

L’output di questo codice è:

ABC

Anche se non c’è nulla di intrinsecamente asincrono in toUpperCase(), si può intuire lo scopo di questo schema.

Se in qualsiasi momento vogliamo uscire dall’esecuzione, possiamo chiamare return() sull’oggetto iteratore:

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

Oltre ai valori potete anche lanciare un’eccezione nel generatore. Vedere “Gestione degli errori per i generatori asincroni”.

Casi d’uso per iterabili asincroni e funzioni generatore asincrone

Se le funzioni generatore sono buone per lavorare in modo sincrono con grandi file e sequenze infinite, le funzioni generatore asincrone permettono una nuova terra di possibilità per JavaScript.

In particolare, l’iterazione asincrona facilita il consumo di flussi leggibili. L’oggetto Response di Fetch espone body come un flusso leggibile con getReader(). Possiamo avvolgere un tale flusso con un generatore asincrono, e successivamente iterare su di esso con for await...of.

Async iterators and generators di Jake Archibald ha un mucchio di begli esempi.

Altri esempi di flussi sono i flussi di richieste con Fetch.

Al momento in cui scrivo non c’è nessuna API del browser che implementa Symbol.asyncIterator, ma la specifica Stream sta per cambiare questo.

In Node.js la recente API Stream gioca bene con i generatori asincroni e l’iterazione asincrona.

In futuro saremo in grado di consumare e lavorare senza problemi con flussi scrivibili, leggibili e trasformatori sul lato client.

Altre risorse:

    >

  • La Stream API.

Chiude

Termini e concetti chiave che abbiamo trattato in questo post:

ECMAScript 2015:

  • iterabile.
  • iteratore.

Questi sono gli elementi costitutivi delle funzioni generatrici.

ECMAScript 2018:

  • iterabile asincrono.
  • iteratore asincrono.

Questi invece sono i mattoni per le funzioni generatrici asincrone.

Una buona comprensione di iterabili e iteratori può portarvi molto lontano. Non è che lavorerete con le funzioni generatrici e le funzioni generatrici asincrone ogni giorno, ma sono una bella abilità da avere nella vostra cintura degli strumenti.

E voi? Le avete mai usate?

Grazie per aver letto e rimanete sintonizzati su questo blog!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.