Muitas estruturas de front-end do JavaScript (como Angular, React e Vue) têm seus próprios sistemas de reatividade. Compreender os recursos desses sistemas será útil para qualquer desenvolvedor, o ajudará a usar com mais eficiência as estruturas JS modernas.

O material, cuja tradução publicamos hoje, mostra um exemplo passo a passo do desenvolvimento de um sistema de reatividade em JavaScript puro. Este sistema implementa os mesmos mecanismos usados no Vue.
Sistema de reatividade
Para alguém que encontra o sistema de reatividade Vue pela primeira vez, pode parecer uma caixa preta misteriosa. Considere um aplicativo Vue simples. Aqui está a marcação:
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ price*quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> </div>
Aqui está o comando de conexão da estrutura e o código do aplicativo.
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03 } } }) </script>
De alguma forma, Vue descobre que, quando o
price
muda, o mecanismo precisa fazer três coisas:
- Atualize o valor do
price
na página da web. - Recalcule a expressão na qual o
price
é multiplicado pela quantity
e exiba o valor resultante na página. - Chame a função
totalPriceWithTax
e, novamente, coloque o que ela retorna na página.
O que acontece aqui é mostrado na ilustração a seguir.
Como o Vue sabe o que fazer quando a propriedade price muda?Agora, temos perguntas sobre como o Vue sabe exatamente o que precisa ser atualizado quando o
price
muda e como o mecanismo rastreia o que está acontecendo na página. O que você pode observar aqui não se parece com um aplicativo JS comum.
Talvez isso ainda não seja óbvio, mas o principal problema que precisamos resolver aqui é que os programas JS geralmente não funcionam assim. Por exemplo, vamos executar o seguinte código:
let price = 5 let quantity = 2 let total = price * quantity
O que você acha que será exibido no console? Como nada é usado aqui, exceto o JS comum,
10
chegarão ao console.
O resultado do programaE ao usar os recursos do Vue, em uma situação semelhante, podemos implementar um cenário em que o valor
total
é recontado quando as variáveis de
price
ou
quantity
mudam. Ou seja, se o sistema de reatividade fosse usado na execução do código acima, não 10, mas 40 seriam exibidos no console:
Saída do console gerada por código hipotético usando um sistema de reatividadeO JavaScript é uma linguagem que pode funcionar tanto procedural quanto orientada a objetos, mas não possui um sistema de reatividade integrado; portanto, o código que consideramos ao alterar o
price
não exibirá o número 40 no console. Para que o indicador
total
seja recalculado quando o
price
ou a
quantity
mudar, precisaremos criar um sistema de reatividade por conta própria e, assim, alcançar o comportamento de que precisamos. Vamos abrir o caminho para esse objetivo em vários pequenos passos.
Tarefa: armazenamento de regras para cálculo de indicadores
Precisamos de um local para salvar informações sobre como o indicador
total
é calculado, o que nos permitirá recalculá-lo ao alterar os valores das variáveis de
price
ou
quantity
.
▍Solução
Primeiro, precisamos informar ao aplicativo o seguinte: "Aqui está o código que vou executar, salve-o, talvez seja necessário executá-lo outra vez". Então, precisamos executar o código. Posteriormente, se os indicadores de
price
ou
quantity
tiverem sido alterados, será necessário chamar o código salvo para recalcular o
total
. É assim:
O código de cálculo total precisa ser salvo em algum lugar para poder acessá-lo posteriormenteO código que você pode chamar em JavaScript para executar alguma ação é formatado como funções. Portanto, escreveremos uma função que lida com o cálculo do
total
e também criaremos um mecanismo para armazenar funções que precisaremos posteriormente.
let price = 5 let quantity = 2 let total = 0 let target = null target = function () { total = price * quantity } record()
Observe que armazenamos a função anônima na variável de
target
e, em seguida, chamamos a função de
record
. Falaremos sobre isso abaixo. Também gostaria de observar que a função de
target
, usando a sintaxe das funções de seta ES6, pode ser reescrita da seguinte maneira:
target = () => { total = price * quantity }
Aqui está a declaração da função de
record
e a estrutura de dados usada para armazenar as funções:
let storage = [] // target function record () { // target = () => { total = price * quantity } storage.push(target) }
Usando a função de
record
, salvamos a função de
target
(no nosso caso
{ total = price * quantity }
) no array de
storage
, o que nos permite chamar essa função posteriormente, possivelmente usando a função de
replay
, cujo código é mostrado abaixo. Isso nos permitirá chamar todas as funções armazenadas no
storage
.
function replay () { storage.forEach(run => run()) }
Aqui, examinamos todas as funções anônimas armazenadas na matriz de
storage
e executamos cada uma delas.
Então, em nosso código, podemos fazer o seguinte:
price = 20 console.log(total) // 10 replay() console.log(total) // 40
Nem tudo parece tão difícil, parece? Aqui está o código inteiro, cujos fragmentos discutimos acima, caso seja mais conveniente para você finalmente lidar com ele. A propósito, esse código não foi escrito acidentalmente dessa maneira.
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () { storage.push(target) } function replay () { storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total)
É isso que será exibido no console do navegador após o início.
Resultado do códigoDesafio: uma solução confiável para armazenar funções
Podemos continuar anotando as funções de que precisamos quando necessário, mas seria bom se tivéssemos uma solução mais confiável que possa ser dimensionada com o aplicativo. Talvez seja uma classe que mantém uma lista de funções originalmente gravadas na variável de
target
e que recebe notificações se precisarmos executar novamente essas funções.
:Solução: classe de dependência
Uma abordagem para resolver o problema acima é encapsular o comportamento que precisamos em uma classe, que pode ser chamada de dependência. Esta classe implementará o padrão de programação do observador padrão.
Como resultado, se criarmos uma classe JS usada para gerenciar nossas dependências (que estarão próximas de como mecanismos semelhantes são implementados no Vue), pode ser assim:
class Dep { // Dep - Dependency constructor () { this.subscribers = [] // , // notify() } depend () { // record if (target && !this.subscribers.includes(target)){ // target // this.subscribers.push(target) } } notify () { // replay this.subscribers.forEach(sub => sub()) // - } }
Observe que, em vez da matriz de
storage
, agora armazenamos nossas funções anônimas na matriz de
subscribers
. Em vez da função de
record
, o método
depend
é agora chamado. Também aqui, em vez da função de
replay
, a função de
notify
é
notify
. Veja como executar nosso código usando a classe
Dep
:
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend()
Nosso novo código funciona da mesma maneira que antes, mas agora é melhor projetado e parece melhor para reutilização.
A única coisa que parece estranha até agora é trabalhar com uma função armazenada na variável de
target
.
Tarefa: mecanismo para criar funções anônimas
No futuro, precisaremos criar um objeto da classe
Dep
para cada variável. Além disso, seria bom encapsular o comportamento de criar funções anônimas em algum lugar, que deve ser chamado ao atualizar os dados relevantes. Talvez isso nos ajude com uma função adicional, que chamaremos de
watcher
. Isso levará ao fato de que podemos substituir essa construção do exemplo anterior por uma nova função:
let target = () => { total = price * quantity } dep.depend() target()
De fato, uma chamada para a função de
watcher
que substitui esse código ficará assim:
watcher(() => { total = price * quantity })
▍ Solução: função de observador
Dentro da função
watcher
, cujo código é apresentado abaixo, podemos executar várias ações simples:
function watcher(myFunc) { target = myFunc // target myFunc dep.depend() // target target() // target = null // target }
Como você pode ver, a função
watcher
usa, como argumento, a função
myFunc
, grava-a na variável de
target
global, chama
dep.depend()
para adicionar essa função à lista de assinantes, chama essa função e redefine a variável de
target
.
Agora obtemos todos os mesmos valores 10 e 40 se executarmos o seguinte código:
price = 20 console.log(total) dep.notify() console.log(total)
Talvez você esteja se perguntando por que implementamos o
target
como uma variável global, em vez de passar essa variável para nossas funções, se necessário. Temos boas razões para fazer exatamente isso, depois você entenderá.
Tarefa: próprio objeto Dep para cada variável
Temos um único objeto da classe
Dep
. E se precisarmos que cada uma de nossas variáveis tenha seu próprio objeto de classe
Dep
? Antes de continuarmos, vamos mover os dados com os quais trabalhamos para as propriedades do objeto:
let data = { price: 5, quantity: 2 }
Imagine por um momento que cada uma de nossas propriedades (
price
e
quantity
) tenha seu próprio objeto interno da classe
Dep
.
Propriedades de preço e quantidadeAgora podemos chamar a função
watcher
assim:
watcher(() => { total = data.price * data.quantity })
Como trabalhamos aqui com o valor da propriedade
data.price
, precisamos que o objeto da classe
Dep
da propriedade
price
coloque uma função anônima (armazenada no
target
) em sua matriz de assinantes (chamando
dep.depend()
). Além disso, como estamos trabalhando com
data.quantity
, precisamos do objeto
Dep
da propriedade
quantity
para colocar uma função anônima (novamente, armazenada no
target
) em sua matriz de assinantes.
Se você descrever isso na forma de um diagrama, obterá o seguinte.
Funções caem em matrizes de assinantes de objetos da classe Dep correspondentes a propriedades diferentesSe tivermos mais uma função anônima na qual trabalhamos apenas com a propriedade
data.price
, a função anônima correspondente deve ir apenas para o depósito do objeto dessa propriedade.
Observadores adicionais podem ser adicionados a apenas uma das propriedades disponíveis.Quando você pode precisar chamar
dep.notify()
para funções inscritas nas alterações na propriedade
price
? Isso será necessário ao alterar o
price
. Isso significa que, quando nosso exemplo estiver completamente pronto, o código a seguir deverá funcionar para nós.
Aqui, ao alterar o preço, você precisa chamar dep.notify () para todas as funções inscritas na alteração de preçoPara que tudo funcione dessa maneira, precisamos interceptar eventos de acesso à propriedade (no nosso caso, é
price
ou
quantity
). Isso permitirá, quando isso acontecer, salvar a função de
target
em uma matriz de assinantes e, quando a variável correspondente for alterada, executar a função armazenada nessa matriz.
Solução: Object.defineProperty ()
Agora, precisamos nos familiarizar com o método ES5 padrão
Object.defineProperty (). Permite atribuir getters e setters às propriedades dos objetos. Permita-me, antes de prosseguirmos para seu uso prático, demonstrar a operação desses mecanismos com um exemplo simples.
let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { // price get() { // console.log(`I was accessed`) }, set(newVal) { // console.log(`I was changed`) } }) data.price // data.price = 20 //
Se você executar esse código no console do navegador, ele exibirá o texto a seguir.
Resultados de Getter e SetterComo você pode ver, nosso exemplo simplesmente imprime algumas linhas de texto no console. No entanto, ele não lê ou define valores, pois redefinimos a funcionalidade padrão de getters e setters. Restauraremos a funcionalidade desses métodos. Nomeadamente, espera-se que os getters retornem os valores dos métodos correspondentes e os setters os definam. Portanto, adicionaremos uma nova variável,
internalValue
, ao código, que usaremos para armazenar o valor atual do
price
.
let data = { price: 5, quantity: 2 } let internalValue = data.price // Object.defineProperty(data, 'price', { // price get() { // console.log(`Getting price: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting price to: ${newVal}`) internalValue = newVal } }) total = data.price * data.quantity // data.price = 20 //
Agora que o getter e o setter funcionam da maneira que deveriam funcionar, o que você acha que entrará no console quando esse código for executado? Veja a figura a seguir.
Saída de dados para o consolePortanto, agora temos um mecanismo que permite receber notificações ao ler valores de propriedade e quando novos valores são gravados neles. Agora, depois de refazer um pouco o código, podemos equipar getters e setters com todas as propriedades do objeto de
data
. Aqui vamos usar o método
Object.keys()
, que retorna uma matriz de chaves do objeto passado para ele.
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // data let internalValue = data[key] Object.defineProperty(data, key, { get() { console.log(`Getting ${key}: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting ${key} to: ${newVal}`) internalValue = newVal } }) }) let total = data.price * data.quantity data.price = 20
Agora todas as propriedades do objeto de
data
têm getters e setters. É isso que aparece no console após a execução desse código.
Saída de dados para o console por getters e settersMontagem do sistema de reatividade
Quando um fragmento de código como
total = data.price * data.quantity
e
total = data.price * data.quantity
valor da propriedade
price
, precisamos da propriedade
price
para "lembrar" a função anônima correspondente (
target
no nosso caso). Como resultado, se a propriedade
price
for alterada, ou seja, configurada para um novo valor, isso levará a uma chamada para esta função para repetir as operações executadas por ela, pois sabe que uma determinada linha de código depende dela. Como resultado, as operações executadas em getters e setters podem ser imaginadas da seguinte maneira:
- Getter - você precisa se lembrar da função anônima, que chamaremos novamente quando o valor for alterado.
- Setter - é necessário executar a função anônima armazenada, o que levará a uma alteração no valor resultante correspondente.
Se você usa a classe
Dep
já conhecida por você nesta descrição, obtém o seguinte:
- Ao ler um valor de propriedade,
dep.depend()
é chamado para salvar a função de target
atual. - Quando um valor é gravado em uma propriedade,
dep.notify()
é chamado para reiniciar todas as funções armazenadas.
Agora vamos combinar essas duas idéias e, finalmente, chegaremos ao código que nos permite alcançar nosso objetivo.
let data = { price: 5, quantity: 2 } let target = null // - , class Dep { constructor () { this.subscribers = [] } depend () { if (target && !this.subscribers.includes(target)){ this.subscribers.push(target) } } notify () { this.subscribers.forEach(sub => sub()) } } // , // Object.keys(data).forEach(key => { let internalValue = data[key] // // Dep const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() // target return internalValue }, set(newVal) { internalValue = newVal dep.notify() // } }) }) // watcher dep.depend(), // function watcher(myFunc){ target = myFunc target() target = null } watcher(() => { data.total = data.price * data.quantity })
Vamos experimentar esse código no console do navegador.
Experiências de código prontasComo você pode ver, funciona exatamente como precisamos! As propriedades de
price
e
quantity
tornaram-se reativas! Todo o código responsável por gerar o
total
quando o
price
ou a
quantity
alterados repetidamente.
Agora, depois de escrevermos nosso próprio sistema de reatividade, esta ilustração da documentação do Vue parecerá familiar e compreensível para você.
Sistema de reatividade VueVê este lindo círculo roxo que diz
contendo getters e setters? Agora ele deve estar familiarizado com você. Cada instância do componente possui uma instância do método observador (círculo azul), que coleta dependências em getters (linha vermelha). Quando, posteriormente, o setter é chamado, ele notifica o método observador, o que leva à nova renderização do componente. Aqui está o mesmo esquema, fornecido com explicações conectando-o ao nosso desenvolvimento.
Diagrama de reatividade Vue com explicaçõesAcreditamos que agora, depois de escrevermos nosso próprio sistema de reatividade, esse esquema não precisará de explicações adicionais.
Obviamente, no Vue, tudo isso é mais complicado, mas agora você deve entender o mecanismo subjacente aos sistemas de reatividade.
Sumário
Depois de ler este material, você aprendeu o seguinte:
- Como criar uma classe
Dep
que coleta funções usando o método depend
e, se necessário, as chama novamente usando o método de notify
. - Como criar uma função de
watcher
que permite controlar o código que executamos (esta é a função de target
), que talvez você precise salvar no objeto da classe Dep
. - Como usar o método
Object.defineProperty()
para criar getters e setters.
Tudo isso, compilado em um único exemplo, levou à criação de um sistema de responsividade em JavaScript puro, entendendo que você pode entender os recursos do funcionamento de tais sistemas usados em estruturas da Web modernas.
Caros leitores! Se, antes de ler este material, você mal imaginou as características dos mecanismos dos sistemas de reatividade, diga-me, agora você conseguiu lidar com eles?