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

Ciao a tutti. Qui è Sergey Omelnytsky. Di recente ho tenuto uno streaming sulla programmazione reattiva, in cui 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. Iniziamo quindi con le definizioni: cosa sono lo stack e la coda?
Stack è una collezione i cui elementi vengono acquisiti secondo il principio «ultimo ad entrare, primo ad uscire» LIFO
Coda è una collezione i cui elementi vengono acquisiti secondo il principio «primo ad entrare, primo ad uscire» FIFO

Ok, continuiamo.

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

JavaScript è un linguaggio di programmazione a thread singolo. Questo significa che ha solo un thread 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; le altre operazioni devono aspettare il loro turno nello stack fino a quando non vengono chiamate.

Stack delle chiamate — è una struttura dati che, semplificando, registra informazioni sul punto in cui ci troviamo nel programma. Se entriamo in una funzione, mettiamo una registrazione di essa in cima allo stack. Quando usciamo dalla funzione, estraiamo l'elemento in cima allo stack e torniamo al punto da cui abbiamo chiamato quella funzione. Questo è tutto ciò che fa uno stack. Ora, c'è una domanda molto interessante. Come funziona l'asynchronousità in JavaScript?

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

In realtà, oltre allo stack, nei browser esiste una coda speciale per gestire il cosiddetto WebAPI. Le funzioni in 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 al momento c'è almeno un elemento nello stack, non possono entrare nello stack. È proprio per questo motivo che le chiamate alle funzioni con timeout spesso non sono precise nel tempo, poiché la funzione non può entrare nello stack finché esso è pieno.

Consideriamo il seguente esempio e seguiamo la sua 'esecuzione' passo per passo. Andiamo a vedere cosa succede nel sistema durante questo processo.

console.log('Ciao');
setTimeout(function cb1() {
    console.log('cb1');
}, 5000);
console.log('Arrivederci');

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

1) All'inizio non succede nulla. La console del browser è vuota, lo stack delle chiamate è vuoto.

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

2) Poi il comando console.log(‘Ciao’) 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) Successivamente, console.log(‘Ciao’) 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 fa parte delle Web API. Inizierà il conto alla rovescia.

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

7) Il comando setTimeout(function cb1() {… }) ha completato l'esecuzione e viene rimosso dallo stack delle chiamate.

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

8) Il comando console.log(‘Arrivederci’) viene aggiunto allo stack delle chiamate.

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

9) Il comando console.log(‘Arrivederci’) viene eseguito.

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

10) Il comando console.log(‘Arrivederci’) viene rimosso dallo stack delle chiamate.

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

11) Dopo che sono trascorsi almeno 5000 ms, il timer termina e colloca 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 mette 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 azione:

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

Bene, abbiamo esplorato come l'asincronia è implementata in JavaScript. Ora parliamo brevemente dell'evoluzione del codice asincrono.

Evoluzione del codice asincrono.

a(function (risultatiDaA) {
    b(risultatiDaA, function (risultatiDaB) {
        c(risultatiDaB, function (risultatiDaC) {
            d(risultatiDaC, function (risultatiDaD) {
                e(risultatiDaD, function (risultatiDaE) {
                    f(risultatiDaE, function (risultatiDaF) {
                        console.log(risultatiDaF);
                    })
                })
            })
        })
    })
});

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. È tutto divertente e spassoso fino a quando non si trasforma in tristezza e malinconia. Perché?

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

Con l'arrivo delle Promise, la situazione è migliorata un po'.

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

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

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

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

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

  • Sono emerse catene di promesse, il che ha migliorato la leggibilità del codice.
  • È stato introdotto un metodo separato per la gestione degli errori.
  • È stata introdotta la possibilità di esecuzione parallela con Promise.all.
  • Possiamo gestire la nidificazione asincrona utilizzando async/await.

Ma la promessa ha i suoi limiti. Ad esempio, una promessa non può essere annullata senza complicate manovre e, cosa più importante, lavora con un solo valore.

Ecco, siamo arrivati gradualmente alla programmazione reattiva. Stanchi? Bene, potete andare a preparare un tè, riflettere e tornare a continuare a leggere. Io intanto proseguirò.

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

Programmazione reattiva — una paradigma di programmazione focalizzata sui flussi di dati e sulla diffusione delle modifiche. Analizziamo più nel dettaglio cosa si intende per flusso di dati.

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

const eventsArray = [];

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

Immagina di avere un campo di input. Creiamo un array e, per ogni evento keyup sull'input, salveremo l'evento nel nostro array. È importante notare che il nostro array è ordinato per tempo, cioè l'indice degli eventi più recenti è maggiore di quello degli eventi più vecchi. Questo array rappresenta una versione semplificata del flusso dei dati, ma non è ancora un flusso. Affinché questo array possa essere definito come un flusso, deve in qualche modo essere in grado di informare gli iscritti che sono arrivati nuovi dati. Così siamo arrivati 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 per tempo, che può comunicare che i dati sono cambiati. Ora immagina quanto sia comodo scrivere codice in cui un'azione richiede di innescare più eventi in diverse parti del codice. Basta iscriversi al flusso e ci informerà automaticamente quando ci sono modifiche. E questo è ciò che fa 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 un tipo principale Observable, diversi tipi ausiliari (Observer, Schedulers, Subjects) e operatori per lavorare con eventi come collezioni (map, filter, reduce, every e simili da JavaScript Array).

Esploriamo i concetti fondamentali di questa libreria.

Observable, Observer, Producer

Observable è il primo tipo di base che esamineremo. Questa classe contiene la parte fondamentale dell'implementazione di RxJs. È collegata a un flusso osservabile, al quale è possibile iscriversi utilizzando il metodo subscribe.

In Observable è implementato un meccanismo ausiliario per la creazione di aggiornamenti, chiamato Observer. La sorgente dei valori per l'Observer si chiama Producer. Questo può essere un array, un iteratore, un web socket, un evento, ecc. Quindi si può dire che l'observable è un conduttore tra Producer e Observer.

Observable gestisce tre tipi di eventi per l'Observer:

  • next – nuovi dati
  • error – un errore, se la sequenza è terminata a causa di una situazione eccezionale. Questo evento implica anch'esso la conclusione della sequenza.
  • completo — segnale di fine sequenza. Ciò significa che non ci saranno più nuovi dati.

Diamo un'occhiata alla demo:

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

Inizialmente elaboreremo i valori 1, 2, 3, e dopo 1 secondo riceveremo 4 e chiuderemo il nostro flusso.

Pensieri ad alta voce

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

Abbonamento

Quando ci abboniamo a un flusso, creiamo una nuova classe abbonamento, che ci consente di annullare l'abbonamento usando il metodo annulla iscrizione. Possiamo anche raggruppare gli abbonamenti tramite il metodo add. E, ovviamente, possiamo anche disaggregare i flussi tramite rimuovi. I metodi add e remove accettano come input un altro abbonamento. È importante notare che quando ci disiscriviamo, ci disiscriveremo anche da tutti gli abbonamenti secondari come se avessimo chiamato il metodo annulla iscrizione su di essi. Andiamo avanti.

Tipi di flussi

CALDOFREDDO
Il produttore viene creato all'esterno dell'observableIl produttore viene creato all'interno dell'observable
I dati vengono trasmessi al momento della creazione dell'observableI dati vengono comunicati al momento dell'iscrizione
È necessaria una logica aggiuntiva per la disiscrizioneIl flusso si conclude autonomamente
Utilizza una relazione uno-a-moltiUtilizza una relazione uno-a-uno
Tutti gli abbonamenti hanno un valore comuneGli abbonamenti sono indipendenti
I dati possono andare persi se non c'è abbonamentoRiproduce tutti i valori dello stream per un nuovo abbonamento

Se dovessi fare un'analogia, immaginerai lo stream caldo come un film al cinema. Il momento in cui arrivi determina da dove inizi a guardare. Lo stream freddo lo paragonerei a una chiamata al supporto tecnico. Chiunque chiami ascolta la registrazione dell'auto-risponditore dall'inizio alla fine, ma puoi riattaccare con un unsubscribe.

Mi piacerebbe sottolineare che esistono anche i cosiddetti stream warm (una definizione che ho trovato raramente e solo in comunità straniere) — è uno stream che si trasforma da freddo a caldo. Nasce quindi la domanda: dove usarlo?)) Farò 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 con asyncPipe. Se utilizzo questo pipe più volte, tornando alla definizione di flusso freddo, ogni pipe richiederà dati dal server, il che è, per così dire, strano. Ma se trasformo un flusso freddo in uno caldo, la richiesta avverrà solo una volta.

In generale, comprendere i tipi di flussi è abbastanza complesso per i principianti, ma è importante.

Operatori

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 offrono la possibilità di lavorare con i flussi. Aiutano a controllare gli eventi che avvengono in un Observable. Esamineremo alcuni dei più popolari, mentre ulteriori dettagli sugli operatori possono essere trovati nei link delle informazioni utili.

Operatori — of

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

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

Operatori — filter

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

L'operatore di filtraggio filter, come si può intuire dal nome, filtra il segnale del flusso. Se l'operatore restituisce vero, allora lo lascia passare.

Operatori — take

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

take — Riceve il numero di emissioni dopo il quale la sequenza si completa.

Operatori — debounceTime

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

debounceTime — scarta i valori emessi che arrivano in un intervallo di tempo specificato tra le emissioni — dopo il tempo stabilito, 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),  // Aspetta 700ms di valori prima di elaborare
  take(3)
);  

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

Operatori — takeWhile

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

Emette valori finché takeWhile non restituisce false, dopodiché si disiscrive dalla sequenza.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emissione di 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 combineLatest è simile a promise.all. Combina più flussi in uno. Dopo che ogni flusso ha emesso almeno un valore, otteniamo gli ultimi valori da ciascuno sotto forma di array. Inoltre, dopo qualsiasi emissione dai 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;
  // Emesso un valore ogni 1000ms
  setInterval(() => {
    observer.next('a: ' + i++);
  }, 1000);
});

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emesso 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 basato su questi valori. Se un valore non proviene da un flusso, il gruppo non sarà formato.

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

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

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

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

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

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

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

Operators — forkJoin

forkJoin unisce anche i flussi, ma emette un valore solo quando tutti i flussi sono completati.

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

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

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

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

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

forkJoin(observer_1, observer_2, observer_3);

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

Operators — map

L'operatore di trasformazione map altera 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;
  // Emission every 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 effettuare effetti collaterali, ovvero azioni che non influenzano la sequenza.

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

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

Abbiamo terminato con gli operatori. Passiamo a Subject.

Pensieri ad alta voce

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

Famiglia di subject

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

  • next – inviare nuovi dati nel flusso
  • error – errore e conclusione del flusso
  • complete – conclusione del flusso
  • subscribe – iscriversi al flusso
  • unsubscribe – disiscriversi dal flusso
  • asObservable – trasformare in observable
  • toPromise – trasformare in promesse

Si distinguono 4 o 5 tipi di subject.

Pensieri ad alta voce

Nel live streaming ho detto 4, ma si è scoperto che ne hanno aggiunto un altro. Come si suol dire, vivi e impara.

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

BehaviorSubject new BehaviorSubject(defaultData) – secondo me, è il tipo di subject più comune. Accetta un valore predefinito. Memorizza sempre i dati dell'ultima emissione, che vengono inviati all'iscrizione. Questa classe ha anche un metodo utile, 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 dei valori che conserverà, e come secondo il tempo durante il quale sono necessari i cambiamenti.

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

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig | Observable, destination?: Observer) – la documentazione non dice nulla su di lui e lo vedo per la prima volta. Chi sa cosa fa, scriva e lo completeremo.

Fatto. Bene, abbiamo esaminato tutto ciò di cui volevo parlare oggi. Spero che queste informazioni siano state utili. Puoi consultare la lista di riferimento 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