# Aprofundando-se na Reatividade

Agora é o momento de mergulhar fundo! Uma das características mais distintas do Vue é o seu discreto sistema de reatividade. Os modelos de dados são proxies de objetos JavaScript. Quando você os modifica, a view é atualizada. Isso faz com que a administração de estado seja simples e intuitiva, mas também é importante entender como isso funciona para evitar algumas pegadinhas. Nesta seção, vamos nos aprofundar em alguns dos detalhes de baixo nível do sistema de reatividade do Vue.

Assista um vídeo gratuito sobre Aprofundando-se na Reatividade no Vue Mastery

# O Que é Reatividade?

Esse termo aparece na programação com uma certa frequência atualmente, mas o que realmente significa quando as pessoas o dizem? Reatividade é um paradigma da programação que permite nos ajustarmos à mudanças de uma maneira declarativa. O exemplo canônico geralmente mostrado, por ser ótimo, é uma planilha do Excel.

Se você colocar o número 2 na primeira célula, e o número 3 na segunda e então utilizar o SUM, a planilha te dará o resultado. Até aqui nada demais. Mas se você atualizar o primeiro número, o SUM é automagicamente atualizado.

O JavaScript, geralmente, não funciona assim. Se escrevêssemos algo semelhante em Javascript:

let val1 = 2
let val2 = 3
let sum = val1 + val2

console.log(sum) // 5

val1 = 3

console.log(sum) // Ainda 5
1
2
3
4
5
6
7
8
9

Ao atualizarmos o primeiro valor, a soma não é ajustada.

Então, como faríamos isso em JavaScript?

Como uma visão geral de alto nível, existem algumas coisas que precisamos ser capazes de fazer:

  1. Rastrear quando um valor é lido. ex: val1 + val2val1 e val2.
  2. Detectar quando um valor muda. ex: Quando atribuímos val1 = 3.
  3. Execute novamente o código que leu o valor originalmente. ex: Execute sum = val1 + val2 novamente para atualizar o valor de sum.

Não podemos fazer isso diretamente usando o código do exemplo anterior, mas voltaremos a este exemplo mais tarde para ver como adaptá-lo para ser compatível com o sistema de reatividade do Vue.

Primeiro, vamos nos aprofundar um pouco mais em como o Vue implementa os requisitos básicos de reatividade descritos acima.

# Como Vue Sabe o Código Executando

Para poder executar nossa soma sempre que os valores mudarem, a primeira coisa que precisamos fazer é envolvê-la em uma função:

const updateSum = () => {
  sum = val1 + val2
}
1
2
3

Mas como informamos o Vue sobre essa função?

O Vue mantém o controle de qual função está sendo executada usando um efeito. Um efeito é um wrapper em torno da função que inicia o rastreamento logo antes de a função ser chamada. O Vue sabe qual efeito está sendo executado em um determinado ponto e pode executá-lo novamente quando necessário.

Para entender melhor, vamos tentar implementar algo semelhante nós mesmos, sem o Vue, para ver como pode funcionar.

O que precisamos é de algo que possa envolver nossa soma, assim:

createEffect(() => {
  sum = val1 + val2
})
1
2
3

Precisamos de createEffect para acompanhar quando a soma está sendo executada. Podemos implementar algo assim:

// Mantém uma pilha de efeitos em execução
const runningEffects = []

const createEffect = fn => {
  // Envolve o fn passado em uma função de efeito
  const effect = () => {
    runningEffects.push(effect)
    fn()
    runningEffects.pop()
  }

  // Executa automaticamente o efeito imediatamente
  effect()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Quando nosso efeito é chamado, ele se adiciona ao array runningEffects, antes de chamar fn. Qualquer coisa que precise saber qual efeito está sendo executado no momento pode verificar esse array.

Os efeitos atuam como ponto de partida para muitos recursos importantes. Por exemplo, tanto a renderização do componente quanto os dados computados usam efeitos internamente. Sempre que algo responde magicamente a alterações de dados, você pode ter certeza de que foi envolvido em um efeito.

Embora a API pública do Vue não inclua nenhuma maneira de criar um efeito diretamente, ela expõe uma função chamada watchEffect que se comporta muito como a função createEffect do nosso exemplo. Discutiremos isso com mais detalhes mais adiante no guia.

Mas saber qual código está sendo executado é apenas uma parte do quebra-cabeça. Como o Vue sabe quais valores o efeito usa e como ele sabe quando eles mudam?

# Como o Vue Rastreia Essas Mudanças

Não podemos rastrear reatribuições de variáveis ​​locais como aquelas em nossos exemplos anteriores, simplesmente não há mecanismo para fazer isso em JavaScript. O que podemos rastrear são as mudanças nas propriedades do objeto.

Quando retornamos um objeto JavaScript simples da função de data de um componente, O Vue envolverá esse objeto em um Proxy (opens new window) com manipuladores para get e set. Os proxies foram introduzidos no ES6 e permitem que o Vue 3 evite algumas das limitações de reatividade que existiam nas versões anteriores do Vue.

A explicação anterior foi breve e requer algum conhecimento de Proxies (opens new window) para ser entendida! Então vamos nos aprofundar um pouco. Há muita literatura sobre Proxies, mas o que você realmente precisa saber é que um Proxy é um objeto que encapsula um outro objeto ou função e permite que você o intercepte.

Nós o utilizamos assim: new Proxy(target, handler)

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property) {
    console.log('interceptado!')
    return target[property]
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// interceptado!
// tacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Aqui, interceptamos tentativas de ler propriedades do objeto-alvo. Uma função manipuladora como essa também é conhecida como armadilha (trap). Existem muitos tipos diferentes de armadilhas disponíveis, cada uma lidando com um tipo diferente de interação.

Indo além de um console.log, nós poderíamos fazer aqui qualquer coisa que desejássemos. Poderíamos até mesmo não retornar o valor real se quiséssemos. Isso é o que torna os Proxies tão poderosos para a criação de APIs.

Um desafio de usar um proxy é a vinculação de this. Gostaríamos que qualquer método fosse vinculado ao Proxy, em vez do objeto-alvo, para que também possamos interceptá-los. Felizmente, o ES6 introduziu outro novo recurso, chamado Reflect, que nos permite fazer esse problema desaparecer com o mínimo de esforço:







 









const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property, receiver) {
    return Reflect.get(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// interceptado!
// tacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

A primeira etapa para implementar reatividade com um proxy é controlar quando uma propriedade é lida. Fazemos isso no manipulador, em uma função chamada track, onde passamos target e property:







 










const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property, receiver) {
    track(target, property)
    return Reflect.get(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// interceptado!
// tacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

A implementação de track não é mostrada aqui. Ele verificará qual efeito está sendo executado e registrará junto de target e property. É assim que Vue sabe que a propriedade é uma dependência do efeito.

Finalmente, precisamos executar novamente o efeito quando o valor da propriedade mudar. Para isso, vamos precisar de um manipulador set em nosso proxy:

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, property, receiver) {
    track(target, property)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    trigger(target, property)
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// interceptado!
// tacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Lembra desta lista de anteriormente? Agora temos algumas respostas sobre como o Vue implementa essas etapas principais:

  1. Rastrear quando um valor for lido: a função track no manipulador get do proxy registra a propriedade e o efeito atual.
  2. Detectar quando esse valor mudar: o manipulador set é chamado no proxy.
  3. Execute novamente o código que leu o valor originalmente: a função trigger procura quais efeitos dependem da propriedade e os executa.

O objeto com o proxy aplicado é invisível para o usuário, mas por baixo dos panos ele possibilita que o Vue faça o rastreamento-de-dependência (dependency-tracking) e a notificação-de-mudança (change-notification) quando propriedades são acessadas ou modificadas. Um problema é que o console de navegadores formatam diferentemente quando objetos de dados convertidos são registrados no log, então pode ser que você queria instalar o vue-devtools (opens new window) para uma interface mais amigável à inspeção.

Se tivéssemos de reescrever nosso exemplo original usando um componente, poderíamos fazer algo assim:

const vm = createApp({
  data() {
    return {
      val1: 2,
      val2: 3
    }
  },
  computed: {
    sum() {
      return this.val1 + this.val2
    }
  }
}).mount('#app')

console.log(vm.sum) // 5

vm.val1 = 3

console.log(vm.sum) // 6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

O objeto retornado por data será encapsulado em um proxy reativo e armazenado como this.$data. As propriedades this.val1 e this.val2 são apelidos para this.$data.val1 e this.$data.val2 respectivamente, então elas passam pelo mesmo proxy.

Vue envolverá a função para sum em um efeito. Quando tentamos acessar this.sum, ele executará esse efeito para calcular o valor. O proxy reativo em torno de $data rastreará se as propriedades val1 e val2 foram lidas enquanto o efeito está sendo executado.

A partir do Vue 3, nossa reatividade agora está disponível em um pacote separado (opens new window). A função que envolve $data em um proxy é chamada de reactive. Podemos chamar isso diretamente nós mesmos, permitindo-nos envolver um objeto em um proxy reativo sem precisar usar um componente:

const proxy = reactive({
  val1: 2,
  val2: 3
})
1
2
3
4

Exploraremos a funcionalidade exposta pelo pacote de reatividade ao longo das próximas páginas deste guia. Isso inclui funções como reactive e watchEffect que já conhecemos, bem como formas de usar outros recursos de reatividade, como computed e watch, sem a necessidade de criar um componente.

# Objetos com Proxy Aplicado

O Vue rastreia internamente todos os objetos que foram transformados em reativos, então ele sempre retorna o mesmo proxy de um mesmo objeto.

Quando um objeto aninhado é acessado através de um proxy reativo, esse objeto é também convertido em um proxy antes de ser retornado:






 
 







const handler = {
  get(target, property, receiver) {
    track(target, property)
    const value = Reflect.get(...arguments)
    if (isObject(value)) {
      // Envolve o objeto aninhado em seu próprio proxy reativo
      return reactive(value)
    } else {
      return value
    }
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# Proxy vs. Identidade Original

O uso do Proxy de fato introduz um novo empecilho a ser considerado: o objeto com o proxy aplicado não é igual ao objeto original em termos de comparação de identidade (===). Por exemplo:

const obj = {}
const wrapped = new Proxy(obj, handlers)

console.log(obj === wrapped) // false
1
2
3
4

Outras operações que dependem de comparações de igualdade estritas também podem ser afetadas, como .includes() ou .indexOf().

A prática recomendada aqui é nunca manter uma referência ao objeto bruto original e trabalhar apenas com a versão reativa:

const obj = reactive({
  count: 0
}) // nenhuma referência ao original
1
2
3

Isso garante que as comparações de igualdade e reatividade se comportem conforme o esperado.

Observe que o Vue não envolve valores primitivos, como números ou strings em um proxy, então você ainda pode usar === diretamente com esses valores:

const obj = reactive({
  count: 0
})

console.log(obj.count === 0) // true
1
2
3
4
5

# Observadores

Toda instância de componente tem uma instância de observador correspondente, que registra quaisquer propriedades "tocadas" durante a renderização do componente como dependências. Depois, quando um setter de uma dependência é disparado, ele notifica o observador, que por sua vez faz o componente re-renderizar.

# Como a Renderização Reage às Mudanças

O template para um componente é compilado em uma função render. A função render cria os VNodes que descrevem como o componente deve ser renderizado. Ele é envolvido em um efeito, permitindo que o Vue rastreie as propriedades que são 'tocadas' enquanto ele está em execução.

Uma função render é conceitualmente muito semelhante a uma propriedade computed. O Vue não rastreia exatamente como as dependências são usadas, ele apenas sabe que elas foram usadas em algum momento enquanto a função estava em execução. Se qualquer uma dessas propriedades for alterada posteriormente, o efeito será acionado novamente, executando novamente a função render para gerar novos VNodes. Estes são então usados ​​para fazer as alterações necessárias no DOM.

Se você está utilizando Vue v2.x ou mais antigo, pode estar interessado em alguns empecilhos da detecção de mudanças que existem nessas versões, explorados em mais detalhes aqui.