19 aprile 2021

Iteratori e generatori asincroni

Gli iteratori asincroni consentono di iterare su dati che arrivano in modo asincrono, a richiesta. Per esempio, quando eseguiamo una serie di download parziali dalla rete. I generatori asincroni ci consentono di semplificare questo processo.

Vediamo prima un semplice esempio per prendere confidenza con la sintassi, dopodiché analizzeremo un caso d’uso reale.

Iteratori asincroni

Gli iteratori asincroni sono simili ai comuni iteratori, con alcune differenze sintattiche.

Gli oggetti iteratori “comuni”, come abbiamo detto nel capitolo iteratori, si presentano in questo modo:

let range = {
  from: 1,
  to: 5,

  // for..of invoca questo metodo una sola volta all'inizio dell'esecuzione

  [Symbol.iterator]() { // called once, in the beginning of for..of

    // ...ritorna l'oggetto iteratore:
    // dopodiché, for..of interagisce solo con questo oggetto,
    // chiedendogli i valori successivi tramite il metodo next()

    return {
      current: this.from,
      last: this.to,

      // next() viene invocato ad ogni iterazione dal ciclo for..of
      next() { // (2)
        // dovrebbe ritornare il valore come un oggetto {done:.., value:..}
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for(let value of range) {
  alert(value); // 1 poi 2, poi 3, poi 4, poi 5
}

Se necessario, rileggersi il capitolo sugli iteratori per avere maggiori dettagli circa gli iteratori comuni.

Per rendere l’oggetto iteratore asincrono:

  1. Dobbiamo usare Symbol.asyncIterator anziché Symbol.iterator.
  2. next() dovrebbe ritornare una promise.
  3. Per iterare sui valori di tale oggetto, dobbiamo usare un ciclo del tipo: for await (let item of iterable).

Rendiamo l’oggetto range iterabile, come nell’esempio precedente ma, questa volta, ritornerà i valori in modo asincrono, uno ogni secondo:

let range = {
  from: 1,
  to: 5,

  // for await..of invoca questo metodo una sola volta all'inizio dell'esecuzione
  [Symbol.asyncIterator]() { // (1)
    // ...ritorna l'oggetto iteratore:
    // dopodich&egrave;, for await..of interagisce solo con questo oggetto,
    // chiedendogli i valori successivi tramite il metodo next()

    return {
      current: this.from,
      last: this.to,

      // next() viene invocato ad ogni iterazione dal ciclo for await..of
      async next() { // (2)
        // dovrebbe ritornare il valore come un oggetto {done:.., value:..}
        // (automaticamente racchiuso in una promise dal momento che siamo un metodo 'async')

        // possiamo utilizzare await all'interno per eseguire codice asincrono:

        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
*/!*

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

Possiamo notare che la struttura è simile a quella dei comuni iteratori:

  1. Per rendere un oggetto iterabile in modo asincrono, esso deve contenere un metodo Symbol.asyncIterator (1).
  2. Questo metodo deve ritornare un oggetto contenente il metodo next(), che ritorna a sua volta una promise (2).
  3. Il metodo next() non deve necessariamente essere async; può essere un metodo normale che ritorna una promise, anche se async ci consentirebbe di utilizzare await, che può tornarci utile. Nell’esempio, abbiamo utilizzato un ritardo di un secondo (3).
  4. Per iterare dobbiamo utilizzare il ciclo for await(let value of range) (4), si tratta di aggiungere “await” dopo il “for”. Questo ciclo invoca il metodo range[Symbol.asyncIterator]() una sola volta, dopodiché il metodo invocherà next() per ottenere i valori.