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

Parte 1. Reatividade e fluxos


Esta série de artigos enfoca a reatividade e sua aplicação em JS usando uma biblioteca maravilhosa como o RxJS.

Série de artigos "Fundamentos da programação reativa usando RxJS":



Para quem é este artigo : basicamente, aqui vou explicar o básico, portanto, o artigo é destinado principalmente para iniciantes nesta tecnologia. Ao mesmo tempo, espero que desenvolvedores experientes possam aprender algo novo por si mesmos. O entendimento exigirá conhecimento de js (es5 / es6).

Motivação : Encontrei o RxJS pela primeira vez quando comecei a trabalhar com angular. Foi então que tive dificuldade em entender o mecanismo de reatividade. Dificuldades foram adicionadas pelo fato de que, no início do meu trabalho, a maioria dos artigos era dedicada à versão antiga da biblioteca. Eu tive que ler muita documentação, vários manuais para pelo menos entender alguma coisa. E só depois de algum tempo comecei a perceber como "tudo está organizado". Para facilitar a vida dos outros, decidi colocar tudo nas prateleiras.

O que é reatividade?


É difícil encontrar uma resposta para um termo aparentemente comum. Em resumo: reatividade é a capacidade de responder a quaisquer alterações. Mas de que mudanças estamos falando? Primeiro de tudo, sobre alterações de dados. Considere um exemplo:

let a = 2; let b = 3; let sum = a + b; console.log(sum); // 5 a = 3; console.log(sum); // 5 -    

Este exemplo demonstra o paradigma de programação imperativo familiar. Diferentemente da abordagem imperativa, a abordagem reativa baseia-se em estratégias de propagação de mudanças por push. A estratégia de envio implica que, no caso de alterações de dados, essas mesmas alterações serão "enviadas" e os dados dependentes deles serão atualizados automaticamente. Aqui está como nosso exemplo se comportaria se uma estratégia de envio fosse aplicada:

 let a = 2; let b = 3; let sum = a + b; console.log(sum); // 5 a = 3; console.log(sum); // 6 -   sum   

Este exemplo mostra uma abordagem reativa. Vale a pena notar que este exemplo não tem nada a ver com a realidade, eu dei apenas para mostrar a diferença nas abordagens. O código reativo em aplicativos do mundo real parecerá muito diferente e, antes de prosseguir para a prática, deveríamos falar sobre outro componente importante da reatividade.

Fluxo de dados


Se você olhar para o termo “programação reativa” na Wikipedia, o site nos dará a seguinte definição: “Programação reativa é um paradigma de programação focado em fluxos de dados e na propagação de alterações”. A partir dessa definição, podemos concluir que a reatividade é baseada em duas “baleias” principais. Eu mencionei a distribuição das mudanças acima, então não iremos nos aprofundar nisso. Mas devemos falar mais sobre fluxos de dados. Vejamos o seguinte exemplo:

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

Ouvimos o evento keyup e colocamos o objeto de evento em nossa matriz. Com o tempo, nossa matriz pode conter milhares de objetos KeyboardEvent. Vale a pena notar que nossa matriz é classificada por tempo - o índice de eventos posteriores é maior que o índice de eventos anteriores. Essa matriz é um modelo de fluxo de dados simplificado. Por que simplificado? Porque a matriz pode armazenar apenas dados. Também podemos iterar a matriz e, de alguma forma, processar seus elementos. Mas a matriz não pode nos dizer que um novo elemento foi adicionado a ela. Para descobrir se novos dados foram adicionados à matriz, teremos que iterá-los novamente.

Mas e se nossa matriz soubesse nos informar que novos dados chegaram nela? Tal matriz poderia com certeza ser chamada de fluxo. Então, chegamos à definição de fluxo. Um fluxo é uma matriz de dados classificados por tempo que pode indicar que os dados foram alterados.

Observável


Agora que sabemos o que são fluxos, vamos trabalhar com eles. No RxJS, os fluxos são representados pela classe Observable. Para criar seu próprio fluxo, chame o construtor dessa classe e passe a função de inscrição como argumento:

 const observable = new Observable(observer => { observer.next(1); observer.next(2); observer.complete(); }) 

Ao chamar o construtor da classe Observable, criamos um novo thread. Como argumento, passamos a função de inscrição para o construtor. A função de assinatura é uma função regular que leva um observador como parâmetro. O próprio observador é um objeto que possui três métodos:

  • next - lança um novo valor no fluxo
  • error - lança um erro no fluxo, após o qual o fluxo termina
  • complete - finaliza o encadeamento

Então, criamos um thread que emite dois valores e termina.

Assinatura


Se rodarmos o código anterior, nada acontecerá. Criaremos apenas um novo fluxo e salvaremos o link na variável observável, mas o próprio fluxo nunca emitirá um único valor. Isso ocorre porque os threads são objetos "preguiçosos" e não fazem nada por si mesmos. Para que nosso fluxo comece a emitir valores e possamos processar esses valores, precisamos começar a "ouvir" o fluxo. Você pode fazer isso chamando o método de inscrição no objeto observável.

 const observer = { next: value => console.log(value), // 1, 2 error: error => console.error(error), // complete: () => console.log("completed") // completed }; observable.subscribe(observer); 

Identificamos nosso observador e descrevemos três métodos para ele: próximo, erro, completo. Os métodos simplesmente registram dados que são passados ​​como parâmetros. Em seguida, chamamos o método de inscrição e passamos nosso observador para ele. No momento da chamada de inscrição, a função de inscrição é chamada, a que passamos para o construtor no estágio de declaração de nosso fluxo. Em seguida, o código da função de assinatura será executado, que passa dois valores ao nosso observador e, em seguida, finaliza o fluxo.

Certamente, muitos têm uma pergunta, o que acontecerá se assinarmos o fluxo novamente? Tudo será o mesmo: o fluxo passará novamente dois valores para o observador e terminará. Cada vez que o método de inscrição é chamado, uma função de inscrição será chamada e todo o seu código será executado novamente. A partir disso, podemos concluir: não importa quantas vezes assinemos o fluxo, nossos observadores receberão os mesmos dados.

Cancelar inscrição


Agora vamos tentar implementar um exemplo mais complexo. Escreveremos um cronômetro que contará segundos do momento da assinatura e os transmitiremos aos observadores.

 const timer = new Observable(observer => { let counter = 0; //  setInterval(() => { observer.next(counter++); //          }, 1000); }); timer.subscribe({ next: console.log //    }); 

O código é bastante simples. Dentro da função de assinatura, declaramos uma variável de contador. Então, usando o fechamento, acessamos a variável a partir da função de seta em setInterval. E a cada segundo passamos a variável para o observador, após o que a incrementamos. Em seguida, assine o fluxo, especifique apenas um método - a seguir. Não se preocupe, porque não anunciamos outros métodos. Nenhum dos métodos de observação é necessário. Podemos até passar um objeto vazio, mas, neste caso, o thread será desperdiçado.

Após o lançamento, veremos os logs cobiçados que aparecerão a cada segundo. Se desejar, você pode experimentar e se inscrever no fluxo várias vezes. Você verá que cada um dos threads será executado independentemente dos outros.

Se você pensar bem, nosso encadeamento será executado durante toda a vida útil de todo o aplicativo, porque não temos lógica para cancelar o setInterval e, na função de inscrição, não há chamada para o método completo. Mas e se precisarmos que o segmento termine?

De fato, tudo é muito simples. Se você olhar para a documentação, poderá ver que o método de inscrição retorna um objeto de inscrição. Este objeto tem um método de cancelamento de inscrição. Nós o chamamos, e nosso observador deixará de receber valores do fluxo.

 const subscription = timer.subscribe({next: console.log}); setTimeout(() => subscription.unsubscribe(), 5000); //   5  

Após o início, veremos que o contador para em 4. Mas, embora tenhamos cancelado a inscrição no fluxo, nossa função setInterval continua funcionando. Ela incrementa nosso contador a cada segundo e o passa para o observador idiota. Para impedir que isso aconteça, você deve escrever a lógica para cancelar o intervalo. Para fazer isso, você precisa retornar uma nova função da função de assinatura na qual a lógica de cancelamento será implementada.

 const timer = new Observable(observer => { let counter = 0; const intervalId = setInterval(() => { observer.next(counter++); }, 1000); return () => { clearInterval(intervalId); } }); 

Agora podemos respirar aliviados. Depois de chamar o método de cancelamento de assinatura, nossa função de cancelamento de assinatura será chamada, o que limpará o intervalo.

Conclusão


Este artigo mostra as diferenças entre as abordagens imperativa e reativa, bem como exemplos de criação de seus próprios fluxos. Na próxima parte, falarei sobre outros métodos para criar threads e como aplicá-los.

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


All Articles