Programmazione asincrona in JavaScript (Callback, Promise, RxJs)
Ciao a tutti. In contatto Omelnitsky Sergey. Non molto tempo fa ho ospitato uno streaming sulla programmazione reattiva, in cui ho parlato dell'asincronia in JavaScript. Oggi vorrei riassumere questo materiale.
Ma prima di iniziare la trattazione principale, occorre fare una premessa. Partiamo quindi dalle definizioni: cosa sono stack e coda?
Pila è una raccolta i cui elementi vengono recuperati su base LIFO “last in, first out”.
Turno è una raccolta i cui elementi sono ottenuti secondo il principio (“first in, first out” FIFO
Ok, continuiamo.
JavaScript è un linguaggio di programmazione a thread singolo. Ciò significa che ha un solo thread di esecuzione e uno stack in cui le funzioni vengono accodate per l'esecuzione. Pertanto, JavaScript può eseguire solo un'operazione alla volta, mentre le altre operazioni attenderanno il loro turno nello stack finché non verranno chiamate.
stack di chiamate è una struttura dati che, in termini semplici, registra informazioni sul punto del programma in cui ci troviamo. Se entriamo in una funzione, spostiamo la sua voce in cima allo stack. Quando torniamo da una funzione, estraiamo l'elemento più in alto dallo stack e finiamo nel punto da cui abbiamo chiamato questa funzione. Questo è tutto ciò che lo stack può fare. E ora una domanda molto interessante. Come funziona allora l'asincronia in JavasScript?
Infatti, oltre allo stack, i browser hanno una coda speciale per lavorare con la cosiddetta WebAPI. Le funzioni di questa coda verranno eseguite in ordine solo dopo che lo stack sarà stato completamente cancellato. Solo dopo vengono posizionati dalla coda nello stack per l'esecuzione. Se al momento c'è almeno un elemento in pila, allora non può entrare in pila. Proprio per questo motivo, chiamare le funzioni per timeout è spesso impreciso nel tempo, poiché la funzione non può passare dalla coda allo stack mentre è pieno.
Diamo un'occhiata al seguente esempio e analizziamolo passo dopo passo. Vediamo anche cosa succede nel sistema.
1) Per ora non è successo nulla. La console del browser è pulita, lo stack di chiamate è vuoto.
2) Quindi il comando console.log('Hi') viene aggiunto allo stack di chiamate.
3) E si è avverato
4) Quindi console.log('Hi') viene rimosso dallo stack di chiamate.
5) Passiamo ora al comando setTimeout(function cb1() {… }). Viene aggiunto allo stack di chiamate.
6) Viene eseguito il comando setTimeout(function cb1() {… }). Il browser crea un timer che fa parte dell'API Web. Eseguirà un conto alla rovescia.
7) Il comando setTimeout(function cb1() {… }) ha completato il suo lavoro e viene rimosso dallo stack di chiamate.
8) Il comando console.log('Bye') viene aggiunto allo stack di chiamate.
9) Viene eseguito il comando console.log('Bye').
10) Il comando console.log('Bye') viene rimosso dallo stack di chiamate.
11) Dopo che sono trascorsi almeno 5000 ms, il timer termina e inserisce la callback cb1 nella coda di callback.
12) Il loop degli eventi prende la funzione cb1 dalla coda di callback e la inserisce nello stack di chiamate.
13) La funzione cb1 viene eseguita e aggiunge console.log('cb1') allo stack di chiamate.
14) Viene eseguito il comando console.log('cb1').
15) Il comando console.log('cb1') viene rimosso dallo stack di chiamate.
16) La funzione cb1 viene rimossa dallo stack di chiamate.
Diamo un'occhiata ad un esempio in dinamica:
Bene, abbiamo esaminato come viene implementata l'asincronia in JavaScript. Parliamo ora brevemente dell'evoluzione del codice asincrono.
L'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 come la conosciamo in JavaScript può essere eseguita solo con funzioni. Possono essere passati come qualsiasi altra variabile ad altre funzioni. Ecco come sono nati i callback. Ed è bello, divertente e fervente, finché non si trasforma in tristezza, malinconia e tristezza. Perché? Sì, è semplice:
Man mano che la complessità del codice cresce, il progetto si trasforma rapidamente in oscuri blocchi multipli nidificati: "l'inferno dei callback".
La gestione degli errori può essere facilmente trascurata.
Non è possibile restituire espressioni con return.
Con l'avvento di Promise la situazione è leggermente migliorata.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 2000);
}).then((result) => {
alert(result);
return result + 2;
}).then((result) => {
throw new Error('FAILED HERE');
alert(result);
return result + 2;
}).then((result) => {
alert(result);
return result + 2;
}).catch((e) => {
console.log('error: ', e);
});
Sono apparse catene di promesse, che hanno migliorato la leggibilità del codice
Esisteva un metodo separato di intercettazione degli errori
Aggiunta esecuzione parallela con Promise.all
Possiamo risolvere l'asincronia annidata con async/await
Ma le promesse hanno i loro limiti. Ad esempio, una promessa non può essere annullata senza ballare con un tamburello, e la cosa più importante è che funzioni con un valore.
Bene, qui ci stiamo avvicinando senza intoppi alla programmazione reattiva. Stanco? Bene, la cosa buona è che puoi andare a preparare alcuni gabbiani, fare un brainstorming e tornare per leggere di più. E continuerò.
Programmazione reattiva - un paradigma di programmazione incentrato sui flussi di dati e sulla propagazione dei cambiamenti. Diamo uno sguardo più da vicino a cos'è un 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 per ogni keyup dell'evento di input, memorizzeremo l'evento nel nostro array. Allo stesso tempo, vorrei notare che il nostro array è ordinato in base al tempo, ad es. l'indice degli eventi successivi è maggiore dell'indice degli eventi precedenti. Tale array è un modello di flusso di dati semplificato, ma non è ancora un flusso. Affinché questo array possa essere chiamato in modo sicuro un flusso, deve essere in grado di informare in qualche modo gli abbonati che sono arrivati nuovi dati. Arriviamo così alla definizione di flusso.
ruscello è un array di dati ordinati per ora che può indicare che i dati sono cambiati. Ora immagina quanto diventa conveniente scrivere codice in cui è necessario attivare più eventi in diverse parti del codice per un'azione. Ci iscriviamo semplicemente allo streaming e ci dirà quando si verificano i cambiamenti. E la libreria RxJs può farlo.
RxJS è una libreria per lavorare con programmi asincroni e basati su eventi utilizzando sequenze osservabili. La libreria fornisce il tipo principale Osservabile, diversi tipi di helper (Osservatore, pianificatori, soggetti) e operatori per lavorare con gli eventi come con le collezioni (mappare, filtrare, ridurre, ogni e simili da JavaScript Array).
Comprendiamo i concetti di base di questa libreria.
Osservabile, Osservatore, Produttore
Osservabile è il primo tipo di base che esamineremo. Questa classe contiene la parte principale dell'implementazione RxJs. È associato a un flusso osservabile, a cui è possibile iscriversi utilizzando il metodo di iscrizione.
Observable implementa un meccanismo ausiliario per la creazione di aggiornamenti, il cosiddetto Osservatore. Viene chiamata la fonte dei valori per un osservatore Produttore. Potrebbe trattarsi di un array, un iteratore, un socket web, qualche tipo di evento, ecc. Quindi possiamo dire che l'osservabile è un filo conduttore tra Produttore e Osservatore.
Observable gestisce tre tipi di eventi Observer:
successivo – nuovi dati
error - Un errore se la sequenza è terminata a causa di un'eccezione. questo evento implica anche la fine della sequenza.
completo: un segnale sulla fine della sequenza. Ciò significa che non ci saranno più nuovi dati
Vediamo la demo:
All'inizio elaboreremo i valori 1, 2, 3 e dopo 1 sec. otteniamo 4 e terminiamo il nostro thread.
Pensare ad alta voce
E poi ho capito che era più interessante raccontarlo che scriverlo. 😀
Sottoscrizione
Quando ci iscriviamo a uno stream, creiamo una nuova classe sottoscrizione, che ci dà la possibilità di annullare l'iscrizione con il metodo cancellarsi. Possiamo anche raggruppare gli abbonamenti utilizzando il metodo aggiungere. Bene, è logico che possiamo separare i thread usando rimuovere. I metodi di aggiunta e rimozione accettano una sottoscrizione diversa come input. Vorrei sottolineare che quando annulliamo l'iscrizione, annulliamo l'iscrizione a tutti gli abbonamenti secondari come se chiamassero anche il metodo di annullamento dell'iscrizione. Andare avanti.
Tipi di flussi
HOT
FREDDO
Il produttore viene creato al di fuori dell'osservabile
Il produttore viene creato all'interno dell'osservabile
I dati vengono passati al momento della creazione dell'osservabile
I dati vengono forniti al momento della sottoscrizione.
Serve più logica per annullare l'iscrizione
Il thread termina da solo
Utilizza una relazione uno-a-molti
Utilizza una relazione uno a uno
Tutti gli abbonamenti hanno lo stesso valore
Gli abbonamenti sono indipendenti
I dati possono andare persi se non è presente alcun abbonamento
Riemette tutti i valori del flusso per un nuovo abbonamento
Per fare un'analogia, immaginerei un flusso caldo come un film al cinema. A che punto sei arrivato, da quel momento hai iniziato a guardare. Paragonerei un flusso freddo con una chiamata in quelli. supporto. Qualsiasi chiamante ascolta la registrazione della segreteria telefonica dall'inizio alla fine, ma è possibile riattaccare annullando l'iscrizione.
Vorrei sottolineare che esistono anche i cosiddetti flussi caldi (ho incontrato una definizione del genere estremamente raramente e solo in comunità straniere): questo è un flusso che si trasforma da flusso freddo a caldo. Sorge la domanda: dove usarlo)) Darò un esempio tratto dalla pratica.
Sto lavorando con Angular. Utilizza attivamente rxjs. Per inviare i dati al server, mi aspetto un flusso freddo e utilizzo questo flusso nel modello utilizzando asyncPipe. Se utilizzo questa pipe più volte, tornando alla definizione di flusso freddo, ogni pipe richiederà dati al server, il che è a dir poco strano. E se converto un flusso freddo in uno caldo, la richiesta avverrà una volta.
In generale, comprendere la tipologia dei flussi è abbastanza difficile 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 l'opportunità di lavorare con i flussi. Aiutano a controllare gli eventi che scorrono nell'Osservabile. Considereremo un paio di quelli più popolari e maggiori informazioni sugli operatori possono essere trovate nei collegamenti in informazioni utili.
Operatori - di
Cominciamo con l'operatore ausiliario di. Crea un osservabile basato su un valore semplice.
Filtro operatori
L'operatore filtro, come suggerisce il nome, filtra il segnale del flusso. Se l'operatore restituisce true, salta ulteriormente.
Operatori: prendi
take - Prende il valore del numero di emissioni, dopo il quale lo stream termina.
Operatori-debounceTime
debounceTime - scarta i valori emessi che rientrano nell'intervallo di tempo specificato tra i dati di output - dopo che è trascorso l'intervallo di tempo, emette l'ultimo valore.
const { Observable } = Rx;
const { debounceTime, take } = RxOperators;
Observable.create((observer) => {
let i = 1;
observer.next(i++);
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++)
}, 1000);
// Испускаем значение раз в 1500мс
setInterval(() => {
observer.next(i++)
}, 1500);
}).pipe(
debounceTime(700), // Ожидаем 700мс значения прежде чем обработать
take(3)
);
const { Observable } = Rx;
const { debounceTime, takeWhile } = RxOperators;
Observable.create((observer) => {
let i = 1;
observer.next(i++);
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++)
}, 1000);
}).pipe(
takeWhile( producer => producer < 5 )
);
Operatori-combineLatest
L'operatore combinato combineLatest è in qualche modo simile a promise.all. Combina più flussi in uno solo. Dopo che ogni thread ha effettuato almeno un'emissione, otteniamo i valori più recenti da ciascuno come array. Inoltre, dopo ogni emissione dai flussi combinati, fornirà nuovi valori.
const { combineLatest, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
});
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750мс
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
});
combineLatest(observer_1, observer_2).pipe(take(5));
Operatori-zip
Zip: attende un valore da ciascun flusso e forma un array basato su questi valori. Se il valore non proviene da nessun thread, il gruppo non verrà formato.
const { zip, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
});
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
});
const observer_3 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
});
zip(observer_1, observer_2, observer_3).pipe(take(5));
Operatori - forkJoin
Anche forkJoin unisce i thread, ma emette un valore solo quando tutti i thread sono completi.
const { forkJoin, Observable } = Rx;
const { take } = RxOperators;
const observer_1 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next('a: ' + i++);
}, 1000);
}).pipe(take(3));
const observer_2 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 750
setInterval(() => {
observer.next('b: ' + i++);
}, 750);
}).pipe(take(5));
const observer_3 = Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 500
setInterval(() => {
observer.next('c: ' + i++);
}, 500);
}).pipe(take(4));
forkJoin(observer_1, observer_2, observer_3);
Mappa degli operatori
L'operatore di trasformazione della mappa trasforma il valore di emissione in uno nuovo.
const { Observable } = Rx;
const { take, map } = RxOperators;
Observable.create((observer) => {
let i = 1;
// Испускаем значение раз в 1000мс
setInterval(() => {
observer.next(i++);
}, 1000);
}).pipe(
map(x => x * 10),
take(3)
);
Operatori: condividi, tocca
L'operatore tap ti consente di eseguire effetti collaterali, ovvero qualsiasi azione che non influisce sulla sequenza.
L'operatore del servizio di condivisione può trasformare un flusso freddo in uno caldo.
Gli operatori hanno finito. Passiamo all'Oggetto.
Pensare ad alta voce
E poi sono andato a bere il tè. Sono stanca di questi esempi 😀
Famiglia soggetto
La famiglia di argomenti è un ottimo esempio di hot thread. Queste classi sono una sorta di ibrido che agisce come osservabile e osservatore allo stesso tempo. Poiché l'argomento è un flusso caldo, è necessario annullare l'iscrizione. Se parliamo dei metodi principali, questi sono:
successivo: passaggio di nuovi dati allo stream
error: errore e chiusura del thread
completo: completamento del thread
iscriviti: iscriviti a uno streaming
annullare l'iscrizione: annullare l'iscrizione al thread
asObservable: trasformarsi in un osservatore
toPromise: si trasforma in una promessa
Assegna 4 5 tipi di argomenti.
Pensare ad alta voce
Ho detto 4 nello streaming, ma si è scoperto che ne hanno aggiunto un altro. Come dice il proverbio, vivi e impara.
Oggetto semplice new Subject()- il tipo più semplice di argomenti. Creato senza parametri. Passa i valori che sono arrivati solo dopo la sottoscrizione.
ComportamentoSoggetto new BehaviorSubject( defaultData<T> ) - secondo me il tipo di soggetto più comune. L'input assume il valore predefinito. Salva sempre i dati dell'ultimo numero, che vengono trasmessi al momento dell'iscrizione. Questa classe ha anche un metodo di valore utile che restituisce il valore corrente del flusso.
Riproduci soggetto new ReplaySubject(bufferSize?: number, windowTime?: number) - Facoltativamente, può prendere come primo argomento la dimensione del buffer di valori che memorizzerà in sé e il secondo tempo durante il quale sono necessarie modifiche.
Oggetto asincrono new AsyncSubject() - al momento della sottoscrizione non succede nulla e il valore verrà restituito solo al termine. Verrà restituito solo l'ultimo valore del flusso.
OggettoWebSocket new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - La documentazione non ne parla e io stesso lo vedo per la prima volta. Chissà cosa fa, scriva, aggiungeremo noi.
Uff. Bene, abbiamo considerato tutto ciò che volevo raccontare oggi. Spero che questa informazione sia stata utile. Puoi leggere tu stesso l'elenco della letteratura nella scheda Informazioni utili.