Programação JavaScript assíncrona (retorno de chamada, promessa, RxJs)

Olá pessoal. Em contato Omelnitsky Sergey. Há pouco tempo, conduzi um fluxo de programação reativa, onde conversei sobre assincronia em JavaScript. Hoje eu gostaria de delinear este material.



Mas antes de começarmos o material básico, precisamos fazer um material introdutório. Então, vamos começar com as definições: qual é a pilha e a fila?


Uma pilha é uma coleção cujos elementos são recebidos com base no princípio de "último a entrar, primeiro a sair"


Uma fila é uma coleção cujos elementos são recebidos de acordo com o princípio (“primeiro a entrar, primeiro a sair”) FIFO


Ok, vamos continuar.



JavaScript é uma linguagem de programação de thread único. Isso significa que ele possui apenas um encadeamento de execução e uma pilha na qual as funções são enfileiradas para execução. Portanto, em um determinado momento, o JavaScript pode executar apenas uma operação, enquanto outras operações aguardam sua vez na pilha até serem chamadas.


A pilha de chamadas é uma estrutura de dados que, em termos simples, registra informações sobre o local no programa em que estamos. Se entrarmos em uma função, colocamos um registro sobre ela no topo da pilha. Quando retornamos da função, puxamos o elemento mais alto da pilha e nos descobrimos de onde chamamos essa função. Isso é tudo o que a pilha pode fazer. E agora uma pergunta extremamente interessante. Como funciona a assincronia no JavasScript?



De fato, além da pilha, os navegadores têm uma fila especial para trabalhar com a chamada WebAPI. As funções dessa fila serão executadas em ordem somente após a pilha ser completamente limpa. Somente depois disso eles são enviados da fila para a pilha para execução. Se pelo menos um elemento estiver atualmente na pilha, eles não poderão entrar na pilha. É justamente por isso que a chamada de funções por tempo limite geralmente não é precisa no tempo, pois uma função não pode passar da fila para a pilha enquanto estiver cheia.


Considere o exemplo a seguir e faça sua "execução" passo a passo. Veja também o que acontece no sistema.


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


1) Até agora, nada está acontecendo. O console do navegador está limpo, a pilha de chamadas está vazia.



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



3) E é executado



4) O console.log ('Hi') é removido da pilha de chamadas.



5) Agora vá para o comando setTimeout (função cb1 () {...}). É adicionado à pilha de chamadas.



6) O comando setTimeout (função cb1 () {...}) é executado. O navegador cria um cronômetro que faz parte da API da Web. Ele fará a contagem regressiva.



7) O comando setTimeout (função cb1 () {...}) foi concluído e é removido da pilha de chamadas.



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



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



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



11) Após pelo menos 5000 ms, o timer sai e coloca o retorno de chamada cb1 na fila de retorno de chamada.



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



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



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



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



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


Veja um exemplo em dinâmica:



Bem, aqui examinamos como a assincronia é implementada no JavaScript. Agora vamos falar brevemente sobre a evolução do código assíncrono.


A 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 a conhecemos em JavaScript, só pode ser implementada com funções. Eles podem ser passados ​​como qualquer outra variável para outras funções. Assim, os retornos de chamada nasceram. E é legal, divertido e provocador, até se transformar em tristeza, saudade e tristeza. Porque Sim, tudo é simples:


  • Com a crescente complexidade do código, o projeto rapidamente se transforma em blocos obscurecidos e repetidamente aninhados - "inferno de retorno de chamada".
  • O tratamento de erros pode ser facilmente esquecido.
  • Você não pode retornar expressões com retorno.

Com o Promise, as coisas ficaram um pouco melhores.


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

  • Apareceram cadeias de promessas, o que melhorou a legibilidade do código
  • Um método separado de interceptação de erro apareceu
  • Agora você pode executar em paralelo usando Promise.all
  • Podemos resolver assincronia aninhada com async / waitit

Mas promis tem suas limitações. Por exemplo, uma promessa, sem dançar com um pandeiro, não pode ser desfeita e, o mais importante, funciona com um valor.


Bem, abordamos a programação reativa sem problemas. Esta cansado Bem, o bom é que você pode fazer algumas gaivotas, pensar e voltar a ler mais. E eu continuarei.


Noções básicas de programação reativa


A programação reativa é um paradigma de programação focado nos fluxos de dados e na disseminação da mudança. Vamos dar uma olhada no que é um fluxo de dados.


 //     const input = ducument.querySelector('input'); const eventsArray = []; //      eventsArray input.addEventListener('keyup', event => eventsArray.push(event) ); 

Imagine que temos um campo de entrada. Criamos uma matriz e, para cada digitação do evento de entrada, salvaremos o evento em nossa matriz. Nesse caso, gostaria de observar que nossa matriz é classificada por tempo, ou seja, o índice de eventos posteriores é maior que o índice de eventos anteriores. Essa matriz é um modelo simplificado de fluxo de dados, mas ainda não é um fluxo. Para que esse array seja chamado de fluxo com segurança, ele deve ser capaz de informar de alguma forma aos assinantes que recebeu novos dados. Então chegamos à definição de fluxo.


Fluxo de dados


 const { interval } = Rx; const { take } = RxOperators; interval(1000).pipe( take(4) ) 


Um fluxo é uma matriz de dados classificados por tempo que pode indicar que os dados foram alterados. Agora imagine como é conveniente escrever código no qual você precisa disparar vários eventos em diferentes partes do código em uma ação. Acabamos de assinar o stream e ele nos informará quando as alterações ocorrerem. E a biblioteca RxJs pode fazer isso.



O RxJS é uma biblioteca para trabalhar com programas assíncronos e baseados em eventos usando sequências observáveis. A biblioteca fornece o tipo principal de Observável , vários tipos auxiliares ( Observador, Agendadores, Assuntos ) e operadores de trabalhar com eventos e com coleções ( mapear, filtrar, reduzir, todos e outros da matriz JavaScript).


Vamos dar uma olhada nos conceitos básicos desta biblioteca.


Observador, Observador, Produtor


Observável é o primeiro tipo básico que veremos. Esta classe contém a maior parte da implementação de RxJs. Ele está associado a um fluxo observável, no qual você pode se inscrever usando o método de inscrição.


Observable implementa um mecanismo auxiliar para criar atualizações, o chamado Observer . A fonte de valores para o Observer é chamada Producer . Pode ser uma matriz, um iterador, um soquete da Web, algum tipo de evento, etc. Então, podemos dizer que observável é um condutor entre Produtor e Observador.


Observable manipula três tipos de eventos do Observer:


  • próximo - novos dados
  • error - um erro se a sequência terminar devido a uma exceção. esse evento também envolve a conclusão da sequência.
  • complete - sinaliza sobre a conclusão da sequência. Isso significa que não haverá novos dados.

Vamos ver a demonstração:



No início, processaremos os valores 1, 2, 3 e após 1 segundo. teremos 4 e terminaremos nosso fluxo.


Pensamentos no ouvido

E então percebi que contar era mais interessante do que escrever sobre isso. : D


Assinatura


Quando assinamos um fluxo, criamos uma nova classe de assinatura que nos permite cancelar a inscrição usando o método de cancelamento da inscrição . Também podemos agrupar assinaturas usando o método add . Bem, é lógico que possamos desagrupar os threads com remove . Os métodos de entrada adicionar e remover aceitam outra assinatura. Gostaria de observar que, quando cancelamos a inscrição, cancelamos a inscrição de todas as assinaturas filhas, como se tivessem chamado o método de cancelamento da inscrição. Vá em frente.


Tipos de fluxos


QUENTEFRIO
Produtor criado fora do observávelProdutor criado dentro observável
Os dados são transferidos no momento em que o observável é criado.Os dados são relatados no momento da assinatura
Precisa de mais lógica para cancelar a inscriçãoO encadeamento termina por conta própria
Usa comunicação um para muitosUsa um relacionamento individual
Todas as assinaturas têm o mesmo valor.As assinaturas são independentes
Os dados podem ser perdidos se não houver assinaturaEmite novamente todos os valores de fluxo para uma nova assinatura

Para fazer uma analogia, eu imaginaria um fluxo quente como um filme em uma sala de cinema. Em que ponto você chegou, a partir desse momento e começou a ver. Eu compararia um fluxo frio com uma chamada naqueles. suporte. Todo chamador ouve a secretária eletrônica do início ao fim, mas você pode desligar usando a descadastramento.


Gostaria de observar que ainda existem os chamados fluxos quentes (uma definição que encontrei muito raramente e apenas em comunidades estrangeiras) - esse é um fluxo que se transforma de um fluxo frio em um quente. Surge a questão - onde usar)) Vou dar um exemplo da prática.


Eu trabalho com um angular. Ele usa ativamente o rxjs. Para obter dados para o servidor, espero um fluxo frio e uso esse fluxo no modelo usando asyncPipe. Se eu usar esse canal várias vezes, retornando à definição de fluxo frio, cada canal solicitará dados do servidor, o que é estranho, no mínimo. E se eu converter um fluxo frio em um quente, o pedido ocorrerá uma vez.


Em geral, entender a forma dos fluxos é bastante complicado 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 eventos que ocorrem no observável. Consideraremos alguns dos mais populares, e os operadores podem ser encontrados em mais detalhes usando os links nas informações úteis.


Operadores - de


Começamos com o operador auxiliar de. Ele cria um Observable com base em um valor simples.



Operadores - filtro



O operador do filtro, como o nome indica, filtra o sinal do fluxo. Se o operador retornar verdadeiro, então pulará mais.


Operadores - leve



take - Pega o valor do número de emissões, após o qual o fluxo termina.


Operadores - debounceTime



debounceTime - descarta os valores emitidos que caem no período especificado entre os dados de saída - após o lapso do intervalo de tempo, ele emite o último valor.


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


Operadores - takeWhile



Emite valores até takeWhile retornar false, após o qual será cancelado a inscrição no fluxo.


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


Operadores - combineLatest


O operador combine combineLatest é um pouco semelhante ao promessa.all. Ele combina vários threads em um. Depois que cada thread faz pelo menos uma emissão, obtemos os últimos valores de cada um na forma de uma matriz. Além disso, após qualquer emissão dos fluxos combinados, ele fornecerá novos valores.



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


Operadores - zip


Zip - espera por um valor de cada fluxo e forma uma matriz com base nesses valores. Se o valor não vier de nenhum fluxo, o grupo não será formado.



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


Operadores - forkJoin


forkJoin também concatena os threads, mas somente valoriza quando todos os threads estão completos.



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


Operadores - mapa


O operador de transformação de mapa converte o valor de emissão em um novo.



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


Operadores - compartilhe, toque em


O operador de torneira - permite que você faça efeitos colaterais, ou seja, quaisquer ações que não afetem a sequência.


O operador de compartilhamento de utilidade pode esquentá-lo de um fluxo frio.



Com os operadores terminados. Vamos para o assunto.


Pensamentos no ouvido

E então eu fui beber algumas gaivotas. Estes exemplos me aborreceram: D


Família do sujeito


A família de sujeitos é um excelente exemplo de fluxos quentes. Essas classes são um tipo de híbrido que atua simultaneamente como observável e observador. Como o assunto é um fluxo quente, é necessário cancelar o registro. Se falamos sobre os métodos básicos, então isto:


  • next - transferência de novos dados para o fluxo
  • error - erro e finalização do fluxo
  • complete - término do fluxo
  • subscrever - subscrever o fluxo
  • cancelar inscrição - cancelar a inscrição no fluxo
  • asObservable - transforme-se em um observador
  • toPromise - se transforma em uma promessa

Alocar 4 5 tipos de assunto.


Pensamentos no ouvido

Ele falou no stream 4, mas acabou que eles adicionaram mais um. Como diz o ditado, viva e aprenda.


Assunto simples new Subject() é o tipo mais simples de assunto. É criado sem parâmetros. Passa valores que vieram somente após a assinatura.


BehaviorSubject new BehaviorSubject( defaultData<T> ) - na minha opinião, o tipo mais comum de assunto. A entrada aceita um valor padrão. Ele sempre salva os dados da última emissão, que são transferidos durante a assinatura. Essa classe também possui um método de valor útil que retorna o valor atual do fluxo.


ReplaySubject new ReplaySubject(bufferSize?: number, windowTime?: number) - A entrada pode opcionalmente aceitar o primeiro argumento como o tamanho do buffer de valores que ele armazenará em si e a segunda vez durante a qual precisamos de alterações.


AsyncSubject new AsyncSubject() - nada acontece durante a inscrição e o valor será retornado somente quando concluído. Somente o último valor do fluxo será retornado.


WebSocketSubject new WebSocketSubject(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) - A documentação é silenciosa e eu a vejo pela primeira vez. Quem sabe o que está fazendo, escreva, complete.


Fuf. Bem, aqui consideramos tudo o que eu queria contar hoje. Espero que esta informação tenha sido útil. Você pode se familiarizar com a lista de referências na guia informações úteis.


Informações Úteis


Source: https://habr.com/ru/post/pt462355/


All Articles