Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Olá a todos. Aqui é o Sergey Omelnytsky. Não faz muito tempo, fiz uma transmissão ao vivo sobre programação reativa, onde falei sobre assincronidade em JavaScript. Hoje, gostaria de resumir esse material.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Mas antes de começarmos o conteúdo principal, precisamos fazer uma introdução. Então, vamos começar com definições: o que são pilha e fila?
Pilha — é uma coleção cujos elementos são obtidos segundo o princípio "último a entrar, primeiro a sair" (LIFO).
Fila — é uma coleção cujos elementos são obtidos segundo o princípio "primeiro a entrar, primeiro a sair" (FIFO).

Ok, vamos continuar.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

JavaScript é uma linguagem de programação de thread única. Isso significa que há apenas uma thread de execução e uma pilha, na qual as funções são colocadas em espera para serem executadas. Portanto, em um determinado momento, JavaScript só pode executar uma operação; outras operações ficarão aguardando sua vez na pilha até serem chamadas.

Pilha de chamadas — é uma estrutura de dados que, de forma simplificada, grava informações sobre onde estamos no programa. Se entramos em uma função, colocamos uma entrada sobre ela na parte superior da pilha. Quando retornamos de uma função, removemos o elemento do topo da pilha e voltamos ao ponto de onde chamamos essa função. Isso é tudo o que a pilha faz. Agora, surge uma pergunta muito interessante. Como, então, a assincronidade funciona em JavaScript?

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Na verdade, além da pilha, os navegadores têm uma fila especial para trabalhar com o chamado WebAPI. As funções dessa fila serão executadas em ordem apenas depois que a pilha estiver completamente limpa. Somente após isso, elas são transferidas da fila para a pilha para execução. Se houver pelo menos um elemento na pilha, elas não poderão entrar na pilha. É exatamente por isso que a chamada de funções de timeout muitas vezes não é precisa em termos de tempo, já que a função não pode ir da fila para a pilha enquanto esta estiver cheia.

Vamos considerar o seguinte exemplo e analisá-lo passo a passo. Também veremos o que está acontecendo no sistema durante isso.

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

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

1) Enquanto nada acontece. O console do navegador está limpo, a pilha de chamadas está vazia.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

2) Depois, o comando console.log('Hi') é adicionado à pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

3) E ela é executada

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

4) Em seguida, console.log('Hi') é removido da pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

5) Agora passamos para o comando setTimeout(function cb1() {… }). Ele é adicionado à pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

6) O comando setTimeout(function cb1() {… }) é executado. O navegador cria um temporizador, que é parte da API Web. Ele começará a contagem regressiva.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

7) O comando setTimeout(function cb1() {… }) terminou e é removido da pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

8) O comando console.log('Bye') é adicionado à pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

9) O comando console.log('Bye') é executado.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

10) O comando console.log('Bye') é removido da pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

11) Depois que pelo menos 5000 ms passarem, o temporizador termina e coloca o callback cb1 na fila de callbacks.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

12) O loop de eventos pega a função cb1 da fila de callbacks e a coloca na pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

13) A função cb1 é executada e adiciona console.log('cb1') à pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

14) O comando console.log('cb1') é executado.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

15) O comando console.log('cb1') é removido da pilha de chamadas.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

16) A função cb1 é removida da pilha de chamadas.

Vamos olhar para um exemplo em ação:

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

E assim, examinamos como a assíncronicidade é implementada em JavaScript. Agora, vamos falar brevemente sobre a evolução do código assíncrono.

Evolução do código assíncrono.

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

A programação assíncrona, como conhecemos em JavaScript, pode ser implementada apenas com funções. Elas podem ser passadas como qualquer outra variável para outras funções. Assim, nasceram os callbacks. E isso é legal, divertido e emocionante, até que se transforma em tristeza e desespero. Por quê? É simples:

  • Com o aumento da complexidade do código, o projeto rapidamente se transforma em blocos aninhados difíceis de entender — o 'callback hell'.
  • O tratamento de erros pode ser facilmente negligenciado.
  • Não se pode devolver expressões com return.

Com o surgimento das Promises, a situação melhorou um pouco.

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

  • As cadeias de promessas surgiram, melhorando a legibilidade do código.
  • Um método separado para captura de erros foi introduzido.
  • Agora é possível a execução paralela com Promise.all
  • Podemos resolver a aninhamento assíncrono com async/await

Mas a promessa tem suas limitações. Por exemplo, uma promessa não pode ser cancelada sem muitos truques, e o mais importante — ela trabalha com um único valor.

Assim, chegamos suavemente à programação reativa. Está cansado? Bem, você pode ir fazer um chá, pensar um pouco e voltar a ler. Eu continuarei.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Programação reativa — é uma paradigma de programação focado em fluxos de dados e propagação de mudanças. Vamos analisar mais detalhadamente o que é um fluxo de dados.

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

const eventsArray = [];

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

Vamos imaginar que temos um campo de entrada. Criamos um array e, a cada evento keyup do input, salvamos o evento neste array. Observamos que nosso array está ordenado por tempo, ou seja, o índice dos eventos mais recentes é maior que o índice dos eventos mais antigos. Esse array representa um modelo simplificado de um fluxo de dados, mas ainda não é um fluxo. Para que esse array possa ser considerado um fluxo, ele deve conseguir de alguma forma informar aos assinantes que novos dados foram recebidos. Assim, chegamos à definição de um fluxo.

Fluxo de dados

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

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

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Fluxo — é um array de dados ordenado por tempo que pode comunicar quando os dados foram alterados. Agora imagine como se torna conveniente escrever código em que uma ação requer a chamada de vários eventos em diferentes partes do código. Nós apenas nos inscrevemos no fluxo e ele nos avisa quando ocorrem mudanças. E é isso que a biblioteca RxJs pode fazer.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

RxJS — é uma biblioteca para trabalhar com programas assíncronos e baseados em eventos usando sequências observáveis. A biblioteca fornece o tipo principal Observable, vários tipos auxiliares (Observer, Schedulers, Subjects) e operadores para trabalhar com eventos como coleções (map, filter, reduce, every e semelhantes do Array JavaScript).

Vamos entender os conceitos básicos desta biblioteca.

Observable, Observer, Producer

Observable — é o primeiro tipo básico que examinaremos. Esta classe contém a parte principal da implementação do RxJs. Ela está relacionada ao fluxo observável, no qual você pode se inscrever usando o método subscribe.

No Observable implementa um mecanismo auxiliar para gerar atualizações, conhecido como Observer. A fonte de valores para o Observer é chamada de Producer. Isso pode ser um array, um iterador, um web socket, um evento, etc. Portanto, podemos dizer que o observable é um condutor entre Producer e Observer.

O Observable processa três tipos de eventos do Observer:

  • next – novos dados
  • error – um erro, se a sequência for concluída devido a uma situação excepcional. Este evento também implica a conclusão da sequência.
  • complete — um sinal de que a sequência foi concluída. Isso significa que não haverá mais novos dados

Vamos ver uma demonstração:

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

No começo, processaremos os valores 1, 2, 3, e após 1 segundo, receberemos 4 e finalizaremos nosso fluxo.

Pensando em voz alta

E aqui percebi que era mais interessante contar do que escrever sobre isso. 😀

Subscription

Quando fazemos uma assinatura no fluxo, criamos uma nova classe subscription, que nos permite cancelar a assinatura usando o método unsubscribe. Também podemos agrupar assinaturas usando o método add. E é lógico que podemos desagrupar fluxos usando remover. Os métodos add e remove aceitam outra assinatura como entrada. Vale ressaltar que, quando cancelamos a assinatura, estamos cancelando todas as assinaturas filhas como se tivéssemos chamado o método unsubscribe nelas. Vamos em frente.

Tipos de fluxos

HOTCOLD
O Producer é criado fora do observableO Producer é criado dentro do observable
Os dados são transmitidos no momento da criação do observableOs dados são comunicados no momento da assinatura
É necessária lógica adicional para o cancelamento da assinaturaO fluxo se encerra sozinho
Usa uma relação um-para-muitosUsa uma relação um-para-um
Todas as assinaturas têm um valor únicoAs assinaturas são independentes
Os dados podem ser perdidos se não houver assinaturaReemiti todos os valores do fluxo para uma nova assinatura

Se eu fizer uma analogia, eu representaria um fluxo quente como um filme no cinema. No momento em que você chega, começa a assistir a partir daquele ponto. Um fluxo frio eu compararia a uma chamada para a assistência técnica. Qualquer pessoa que ligar ouve a gravação do correio de voz do começo ao fim, mas você pode desligar com o método unsubscribe.

Gostaria de observar que existem também os chamados fluxos quentes (essa definição que encontrei raramente e apenas em comunidades estrangeiras) — este é um fluxo que se transforma de um fluxo frio em um quente. Surge a pergunta — onde usar isso? Vou dar um exemplo da prática.

Eu trabalho com Angular. Ele utiliza ativamente o RxJS. Para obter dados no servidor, espero um fluxo frio e utilizo esse fluxo no template com o asyncPipe. Se eu usar esse pipe várias vezes, voltando à definição de fluxo frio, cada pipe irá solicitar dados do servidor, o que, para dizer o mínimo, é estranho. E se eu transformar o fluxo frio em quente, a solicitação ocorrerá uma única vez.

Na verdade, a compreensão dos tipos de fluxos é bastante complexa para iniciantes, mas importante.

Operadores

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

Os operadores nos fornecem a capacidade de trabalhar com fluxos. Eles ajudam a controlar os eventos que ocorrem em um Observable. Vamos considerar alguns dos mais populares; para mais detalhes sobre os operadores, você pode consultar os links nas informações úteis.

Operadores — of

Vamos começar com o operador auxiliar of. Ele cria um Observable com base em um valor simples.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — filter

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

O operador de filtragem filter, como o próprio nome sugere, filtra o sinal do fluxo. Se o operador retornar verdadeiro, ele passa adiante.

Operadores — take

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

take — Aceita um número de emissões, após o qual o fluxo é encerrado.

Operadores — debounceTime

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

debounceTime — descarta os valores emitidos que ocorrem dentro de um intervalo de tempo especificado entre as saídas — após o intervalo de tempo, emite o último valor.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emite um valor a cada 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);

  // Emite um valor a cada 1500ms
  setInterval(() => {
    observer.next(i++)
  }, 1500);
}).pipe(
  debounceTime(700),  // Espera 700ms pelos valores antes de processá-los
  take(3)
);  

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — takeWhile

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Emite valores enquanto takeWhile não retornar falso, após o que se desinscreve do fluxo.

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

Observable.create((observer) => {
  let i = 1;
  observer.next(i++);
  // Emite um valor a cada 1000ms
  setInterval(() => {
    observer.next(i++)
  }, 1000);
}).pipe(
  takeWhile(producer => producer < 5)
);  

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — combineLatest

O operador combinado combineLatest é semelhante ao promise.all. Ele combina vários fluxos em um só. Assim que cada fluxo emite pelo menos uma vez, obtemos os últimos valores de cada um na forma de um array. Em seguida, após qualquer emissão dos fluxos combinados, ele retornará novos valores.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

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

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

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emite um valor a cada 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

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

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — zip

Zip espera um valor de cada fluxo e forma um array baseado nesses valores. Se um valor não for recebido de um dos fluxos, o grupo não será formado.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

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

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

const observer_2 = Observable.create((observer) => {
  let i = 1;
  // Emite um valor a cada 750ms
  setInterval(() => {
    observer.next('b: ' + i++);
  }, 750);
});

const observer_3 = Observable.create((observer) => {
  let i = 1;
  // Emite um valor a cada 500ms
  setInterval(() => {
    observer.next('c: ' + i++);
  }, 500);
});

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

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — forkJoin

forkJoin também combina fluxos, mas ele emite um valor somente quando todos os fluxos estão completos.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

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

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

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

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

forkJoin(observer_1, observer_2, observer_3);

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — map

O operador de transformação map converte o valor emitido em um novo.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

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

Observable.create((observer) => {
  let i = 1;
  // Emite um valor a cada 1000ms
  setInterval(() => {
    observer.next(i++);
  }, 1000);
}).pipe(
  map(x => x * 10),
  take(3)
);

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Operadores — share, tap

O operador tap permite fazer efeitos colaterais, ou seja, realizar ações que não afetam a sequência.

O operador utilitário share é capaz de transformar um fluxo frio em um quente.

Programação assíncrona em JavaScript. (Callback, Promise, RxJs)

Terminamos com os operadores. Vamos passar para o Subject.

Pensando em voz alta

E aqui fui eu tomar um chá. Esses exemplos me cansaram 😀

Família de subjects

A família de subjects é um exemplo claro de fluxos quentes. Essas classes são uma espécie de híbrido que atuam simultaneamente como observable e observer. Como o subject é um fluxo quente, é necessário cancelar a inscrição. Falando sobre os principais métodos, temos:

  • next – transmissão de novos dados no fluxo
  • error – erro e finalização do fluxo
  • complete – finalização do fluxo
  • subscribe – inscrever-se no fluxo
  • unsubscribe – cancelar a inscrição no fluxo
  • asObservable – transforma em um observador
  • toPromise – transforma em uma promessa

Diferenciam-se 4 a 5 tipos de subjects.

Pensando em voz alta

Na transmissão, mencionei 4, mas parece que adicionaram mais um. Como diz o ditado, viver é aprender.

Subject simples new Subject()– é o tipo mais simples de subject. É criado sem parâmetros. Transmite valores recebidos apenas após a inscrição.

BehaviorSubject new BehaviorSubject(defaultData) – na minha opinião, é o tipo mais comum de subjects. Recebe um valor padrão como entrada. Sempre mantém os dados da última emissão, que são transmitidos na inscrição. Esta classe também possui um método útil chamado value, que retorna o valor atual do fluxo.

ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) – Como entrada, pode opcionalmente aceitar o tamanho do buffer de valores que manterá como primeiro argumento e, como segundo, o tempo durante o qual as alterações são necessárias.

AsyncSubject new AsyncSubject() – ao se inscrever, nada acontece, e o valor será retornado apenas na finalização. Apenas o último valor do fluxo será retornado.

WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig | Observable, destination?: Observer) – A documentação não menciona nada sobre ele e eu o vejo pela primeira vez. Se alguém souber o que ele faz, escreva, vamos complementar.

Uf, bem, assim discutimos tudo o que eu queria falar hoje. Espero que esta informação tenha sido útil. Você pode consultar a lista de referências na aba de informações úteis.

Informação útil

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster