Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Ciao a tutti. Sono Sergey Omelnitsky. Non molto tempo fa ho tenuto uno streaming sulla programmazione reattiva, dove ho parlato dell'asincronicità in JavaScript. Oggi vorrei riassumere questo materiale.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Ma prima di iniziare il materiale principale, dobbiamo fare un'introduzione. Quindi, iniziamo con le definizioni: che cos'è uno stack e una coda?
Stack è una collezione i cui elementi vengono ottenuti secondo il principio "ultimo entrato, primo uscito" LIFO
Coda è una collezione i cui elementi vengono ottenuti secondo il principio "primo entrato, primo uscito" FIFO

Ok, continuiamo.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

JavaScript è un linguaggio di programmazione a thread singolo. Questo significa che esiste solo un flusso di esecuzione e uno stack in cui le funzioni vengono messe in coda per l'esecuzione. Di conseguenza, in un dato momento JavaScript può eseguire solo un'operazione, mentre le altre operazioni devono attendere il loro turno nello stack finché non vengono chiamate.

Stack delle chiamate è una struttura dati che, semplificando, registra informazioni sul luogo nel programma in cui ci troviamo. Se entriamo in una funzione, posizioniamo una registrazione di essa nella parte superiore dello stack. Quando ritorniamo dalla funzione, estraiamo il più alto elemento dallo stack e ci troviamo nel punto da cui abbiamo chiamato quella funzione. Questo è tutto quello che sa fare uno stack. E ora, una domanda molto interessante. Come funziona allora l'asincronicità in JavaScript?

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

In realtà, oltre allo stack, nei browser esiste una particolare coda per lavorare con il cosiddetto WebAPI. Le funzioni di questa coda verranno eseguite in ordine solo dopo che lo stack sarà completamente vuoto. Solo allora vengono spostate dalla coda allo stack per l'esecuzione. Se attualmente nello stack c'è almeno un elemento, non possono entrare nello stack. È proprio per questo che la chiamata delle funzioni con timeout è spesso imprecisa nel tempo, poiché la funzione non può entrare nello stack dalla coda finché è pieno.

Consideriamo il seguente esempio e affrontiamo il suo "esecuzione" passo dopo passo. Inoltre, vediamo cosa sta accadendo nel sistema.

console.log('Hi);
setTimeout(function cb1() {
    console.log('cb1');
}, 5000);
console.log('Bye');

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

1) Non succede niente. La console del browser è pulita, lo stack delle chiamate è vuoto.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

2) Poi il comando console.log('Hi') viene aggiunto allo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

3) E viene eseguito.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

4) Quindi console.log('Hi') viene rimosso dallo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

5) Ora passiamo al comando setTimeout(function cb1() {… }). Viene aggiunto allo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

6) Il comando setTimeout(function cb1() {… }) viene eseguito. Il browser crea un timer, che è parte delle Web API. Inizierà il conteggio alla rovescia.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

7) Il comando setTimeout(function cb1() {… }) ha terminato l'esecuzione ed è rimosso dallo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

8) Il comando console.log('Bye') viene aggiunto allo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

9) Il comando console.log('Bye') viene eseguito.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

10) Il comando console.log('Bye') viene rimosso dallo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

11) Dopo almeno 5000 ms, il timer termina l'esecuzione e inserisce il callback cb1 nella coda dei callback.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

12) Il ciclo degli eventi prende la funzione cb1 dalla coda dei callback e la inserisce nello stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

13) La funzione cb1 viene eseguita e aggiunge console.log('cb1') allo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

14) Il comando console.log('cb1') viene eseguito.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

15) Il comando console.log('cb1') viene rimosso dallo stack delle chiamate.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

16) La funzione cb1 viene rimossa dallo stack delle chiamate.

Diamo un'occhiata a un esempio in dinamica:

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Così abbiamo esaminato come in JavaScript è realizzata l'asincronia. Ora parliamo brevemente dell'evoluzione del codice asincrono.

Evoluzione del codice asincrono.

a(function (resultsFromA) {
    b(resultsFromA, function (resultsFromB) {
        c(resultsFromB, function (resultsFromC) {
            d(resultsFromC, function (resultsFromD) {
                e(resultsFromD, function (resultsFromE) {
                    f(resultsFromE, function (resultsFromF) {
                        console.log(resultsFromF);
                    })
                })
            })
        })
    })
});

La programmazione asincrona, così come la conosciamo in JavaScript, può essere realizzata solo tramite funzioni. Queste possono essere passate come qualsiasi altra variabile ad altre funzioni. Così sono nati i callback. Ed è divertente, allegro e spensierato, finché non si trasforma in tristezza, malinconia e dolore. Perché? È semplice:

  • Con l'aumento della complessità del codice, il progetto si trasforma rapidamente in blocchi annidati poco comprensibili: il 'callback hell'.
  • La gestione degli errori può essere facilmente trascurata.
  • Non è possibile restituire espressioni con return.

Con l'avvento dei Promise, la situazione è migliorata un po'.

new Promise(function(resolve, reject) {
    setTimeout(() => resolve(1), 2000);

}).then((result) => {
    alert(result);
    return result + 2;

}).then((result) => {
    throw new Error('FALLITO QUI');
    alert(result);
    return result + 2;

}).then((result) => {
    alert(result);
    return result + 2;

}).catch((e) => {
    console.log('errore: ', e);
});

  • Sono state introdotte catene di promesse, il che ha migliorato la leggibilità del codice.
  • È stato introdotto un metodo separato per la gestione degli errori.
  • È stata resa possibile l'esecuzione parallela tramite Promise.all.
  • Possiamo risolvere l'asincronicità annidata usando async/await

Ma un promise ha le sue limitazioni. Ad esempio, un promise, senza particolari trucchi, non può essere annullato e, cosa più importante, lavora con un solo valore.

Ecco, siamo arrivati gradualmente alla programmazione reattiva. Stanchi? Bene, puoi sempre andare a preparare un tè, riflettere e tornare a leggere. Io continuerò.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Programmazione reattiva — è un paradigma di programmazione orientato ai flussi di dati e alla propagazione delle modifiche. Approfondiamo più dettagliatamente cosa si intende per flusso di dati.

// Получаем ссылку на элемент
const input = ducument.querySelector('input');

const eventsArray = [];

// Пушим каждое событие в массив eventsArray
input.addEventListener('keyup',
    event => eventsArray.push(event)
);

Immaginiamo di avere un campo di input. Creiamo un array e ad ogni evento keyup dell’input salveremo l'evento nel nostro array. Desidero sottolineare che il nostro array è ordinato per tempo , cioè  l'indice degli eventi più recenti è maggiore di quello degli eventi precedenti. Questo array rappresenta un modello semplificato di flusso di dati, ma non è ancora un flusso. Affinché questo array possa essere chiamato flusso, deve essere in grado in qualche modo di comunicare agli abbonati che sono stati ricevuti nuovi dati. Così siamo giunti alla definizione di flusso.

Flusso di dati

const { interval1 } = Rx;
const { take } = RxOperators;

interval(1000).pipe(
    take(4)
)

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Flusso — è un array di dati ordinati nel tempo, in grado di comunicare che i dati sono cambiati. Ora immagina quanto diventa comodo scrivere codice in cui, per un'azione, è necessario generare diversi eventi in diverse parti del codice. Ci abboniamo semplicemente al flusso e lui ci informerà da solo quando ci saranno cambiamenti. E questo è ciò che sa fare la libreria RxJs.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

RxJS — è una libreria per lavorare con programmi asincroni e basati su eventi utilizzando sequenze osservabili. La libreria fornisce il tipo principale Observable, alcuni tipi ausiliari (Observer, Schedulers, Subjects) e operatori per lavorare con eventi come collezioni (map, filter, reduce, every e simili dell'Array di JavaScript).

Esaminiamo i concetti di base di questa libreria.

Observable, Observer, Producer

Observable — il primo tipo di base che tratteremo. Questa classe contiene la parte fondamentale dell'implementazione di RxJs. È legata a un flusso osservabile, al quale si può abbonarsi tramite il metodo subscribe.

In Observable viene implementato un meccanismo ausiliario per creare aggiornamenti, noto come Observer. La fonte di valori per l'Observer è chiamata Produttore. Questo può essere un array, un iteratore, un web socket, un evento, ecc. Quindi si può dire che l'observable funge da intermediario tra Producer e Observer.

Observable gestisce tre tipi di eventi dell'Observer:

  • next – nuovi dati
  • error – un errore, se la sequenza è terminata a causa di un'eccezione. Questo evento implica anche la conclusione della sequenza.
  • complete — segnale di conclusione della sequenza. Questo significa che non ci saranno più nuovi dati.

Vediamo una demo:

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

All'inizio elaboreremo i valori 1, 2, 3, e dopo 1 secondo riceveremo 4 e concluderemo il nostro flusso.

Pensando ad alta voce

E qui ho capito che raccontare era più interessante che scriverne. 😀

Subscription

Quando ci iscrivi a un flusso, creiamo un nuovo subscription, che ci consente di annullare l'iscrizione tramite il metodo unsubscribe. Possiamo anche raggruppare le iscrizioni tramite il metodo add. È logico che possiamo disaggregare i flussi tramite remove. I metodi add e remove accettano un'altra iscrizione. Vorrei notare che quando ci disiscrivi, ti disiscrivi da tutte le sottoscrizioni figlie come se avessi chiamato il metodo unsubscribe anche per loro. Andiamo avanti.

Tipi di flussi

HOTCOLD
Il Producer è creato esternamente all'observableIl Producer è creato internamente all'observable
I dati vengono trasmessi al momento della creazione dell'observableI dati vengono comunicati al momento dell'iscrizione
È necessaria logica aggiuntiva per la disiscrizioneIl flusso termina autonomamente
Utilizza la relazione uno-a-moltiUtilizza la relazione uno-a-uno
Tutte le iscrizioni hanno un valore unicoLe iscrizioni sono indipendenti
I dati possono andare persi se non c'è iscrizioneRilascia tutti i valori del flusso per una nuova iscrizione

Se dovessimo fare un'analogia, rappresenterei il flusso caldo come un film al cinema. Da quale momento arrivi, inizi a guardare da quel punto. Il flusso freddo lo paragonerei a una chiamata al supporto tecnico. Chiunque chiami ascolta la registrazione del centralinista dall'inizio alla fine, ma puoi riagganciare tramite unsubscribe.

Vorrei sottolineare che ci sono anche i cosiddetti flussi caldi (questa definizione l'ho incontrata molto raramente e solo in comunità straniere) — è un flusso che si trasforma da un flusso freddo in uno caldo. Si pone quindi la domanda: dove utilizzarlo?)) Porterò un esempio dalla pratica.

Lavoro con Angular. Utilizza attivamente RxJS. Per ottenere dati dal server, mi aspetto un flusso freddo e utilizzo questo flusso nel template tramite asyncPipe. Se utilizzo questo pipe più volte, tornando alla definizione di flusso freddo, ogni pipe richiederà i dati al server, il che è, per dirla in modo educato, strano. Ma se trasformo un flusso freddo in caldo, la richiesta avverrà una sola volta.

In generale, la comprensione dei tipi di flussi è abbastanza complessa per i principianti, ma è importante.

Operators

return this.http.get(`${environment.apiUrl}\/${this.apiUrl}\/trade_companies`)
    .pipe(
        tap(({ data }: TradeCompanyList) => this.companies$$.next(cloneDeep(data))),
        map(({ data }: TradeCompanyList) => data)
    );

Gli operatori ci forniscono maggiori possibilità di lavorare con i flussi. Aiutano a controllare gli eventi che fluiscono negli Observable. Esamineremo alcuni dei più popolari, mentre per maggiori dettagli sugli operatori puoi consultare i link nelle informazioni utili.

Operators — of

Iniziamo con l'operatore ausiliario of. Crea un Observable basato su un valore semplice.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operators — filter

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

L'operatore di filtraggio filter, come si può capire dal nome, filtra il segnale del flusso. Se l'operatore restituisce vero, il segnale passa oltre.

Operators — take

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

take — Accetta il numero di emissioni dopo il quale il flusso termina.

Operators — debounceTime

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

debounceTime — scarta i valori emessi che si presentano entro l'intervallo di tempo specificato tra i dati in uscita — dopo il trascorrere dell'intervallo di tempo emette l'ultimo valore.

const { Observable } = Rx;
const { debounceTime, take } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emissione di un valore ogni 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);

  // Emissione di un valore ogni 1500ms
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Aspettiamo 700ms per il valore prima di elaborarlo
  take(3)
);  

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operators — takeWhile

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Emette valori finché takeWhile non restituisce falso, dopodiché si disconnette dal flusso.

const { Observable } = Rx;
const { debounceTime, takeWhile } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emitto un valore ogni 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);
}).pipe(
  takeWhile(producer => producer < 5)
);  

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operatori — combineLatest

L'operatore combinato combineLatest è simile a promise.all. Combina più flussi in uno. Una volta che ogni flusso ha emesso almeno un valore, otteniamo gli ultimi valori da ciascuno come un array. In seguito, dopo ogni emissione da uno dei flussi combinati, restituirà nuovi valori.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

const { combineLatest, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

combineLatest(observer_1, observer_2).pipe(take(5));

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operatori — zip

Zip attende un valore da ciascun flusso e forma un array in base a questi valori. Se un valore non arriva da un flusso, il gruppo non verrà formato.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

const { zip, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 500ms
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
});

zip(observer_1, observer_2, observer_3).pipe(take(5));

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operatori — forkJoin

forkJoin combina anche i flussi, ma emette un valore solo quando tutti i flussi sono stati completati (complete).

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

const { forkJoin, Observable } = Rx;
const { take } = RxOperators;

const observer_1 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
}).pipe(take(3));

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
}).pipe(take(5));

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 500ms
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
}).pipe(take(4));

forkJoin(observer_1, observer_2, observer_3);

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operatori — map

L'operatore di trasformazione map trasforma il valore emesso in uno nuovo.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

const { Observable } = Rx;
const { take, map } = RxOperators;

Observable.create((observer) => {
  let i = 1;
  // Emitto un valore ogni 1000ms
  setInterval(() => {
    observer.next(i++);
  }, 1000);
}).pipe(
  map(x => x * 10),
  take(3)
);

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Operatori – share, tap

L'operatore tap consente di eseguire effetti collaterali, ovvero qualsiasi azione che non influisce sulla sequenza.

L'operatore utility share è in grado di trasformare un flusso freddo in uno caldo.

Programmazione asincrona in JavaScript. (Callback, Promise, RxJs)

Abbiamo finito con gli operatori. Passiamo al Subject.

Pensando ad alta voce

E qui sono andato a bere un tè. Questi esempi mi hanno esausto 😀

La famiglia dei subject

La famiglia dei subject è un chiaro esempio di flussi caldi. Queste classi sono una sorta di ibrido che funge sia da observable che da observer. Poiché il subject è un flusso caldo, è necessario disiscriversi da esso. Parlando dei principali metodi, abbiamo:

  • next – invio di nuovi dati al flusso
  • error – errore e termine del flusso
  • complete – conclusione del flusso
  • subscribe – iscriversi al flusso
  • unsubscribe – disiscriversi dal flusso
  • asObservable – trasformare in osservabile
  • toPromise – trasformare in promessa

Si distinguono 4-5 tipi di subject.

Pensando ad alta voce

Durante la diretta ho parlato di 4, ma si è scoperto che ne hanno aggiunto un altro. Come si dice, chi vive impara.

Subject semplice new Subject()– il tipo più semplice di subject. Viene creato senza parametri. Trasmette solo i valori ricevuti dopo l'iscrizione.

BehaviorSubject new BehaviorSubject(defaultData<T>) – a mio avviso il tipo di subject più comune. Accetta un valore predefinito. Salva sempre l'ultimo dato emesso, che trasmette all'iscrizione. Questa classe ha anche un metodo utile chiamato value che restituisce il valore attuale del flusso.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) – Può accettare opzionalmente come primo argomento la dimensione del buffer di valori che manterrà e come secondo l'intervallo di tempo per cui sono necessari gli aggiornamenti.

AsyncSubject new AsyncSubject() – all'iscrizione non accade nulla, e il valore verrà restituito solo al termine. Solo l'ultimo valore del flusso verrà restituito.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) – La documentazione tace su di lui e lo vedo per la prima volta. Se qualcuno sa cosa fa, scrivetelo, completeremo.

Uff. Bene, abbiamo esaminato tutto ciò che volevo raccontare oggi. Spero che queste informazioni siano state utili. È possibile consultare autonomamente l'elenco di letture nella scheda informazioni utili.

Informazioni utili

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster