A reatividade do Vue é baseada em "inscrições". Quando um efeito lê uma ref
,
o Vue registra que esse efeito depende desse valor. Quando o valor é atualizado,
o Vue executa novamente esse efeito.
Quando você escreve um bloco <template>
em um arquivo .vue
, o processo de build
transforma aquilo em uma função de renderização (semelhante ao que acontece
com o JSX). Ou seja, se você utiliza uma ref
no seu template, quando essa ref
mudar,
a sua função de template será executada novamente.
Outra forma de criar efeitos é com um watchEffect
. Da mesma forma, se o callback do seu watchEffect
ler alguma ref
, e essa ref
mudar, o callback será chamado novamente.
<script setup>
import { ref, watchEffect } from "vue";
const count = ref(0);
function increaseCount() {
count.value++;
}
watchEffect(() => {
console.log("count increased", count.value)
})
</script>
<template>
<p>Counter is {{ count }}</p>
<button @click="increaseCount">Inc</button>
</template>
Nesse exemplo, cada vez que increaseCount
é chamado, alteramos o valor de count
, o que faz
a função <template>
ser executada novamente, resultando na atualização do DOM.
Como temos um watchEffect
que lê count
, também exibimos uma mensagem de log no console.
Mas o que acontece se modificarmos count
mais de uma vez na mesma função?
function increaseCount() {
count.value++;
count.value++;
count.value++;
count.value++;
}
Felizmente, o Vue já possui uma otimização para isso, executando ambos os efeitos apenas uma vez, mesmo tendo
modificado count
4 vezes. Experimente modificar a função no playground acima.
O Vue usa uma estratégia simples de "agendar" que esses efeitos precisam ser re-executados,
e depois executa esses efeitos em lote. Além de melhorar a performance, evitando que um mesmo watchEffect
execute várias vezes ou que o DOM seja modificado múltiplas vezes desnecessariamente, o Vue também
consegue organizar a ordem em que esses efeitos são executados (efeitos em componentes pais devem acontecer
antes de efeitos em componentes filhos).
Entretanto, o Vue fornece formas de "escapar" desse comportamento quando necessário.
Atuando depois de atualizar o DOM
Você provavelmente já encontrou o seguinte comportamento no Vue: você não consegue acessar um elemento DOM na mesma função que alterou a visibilidade dele.
<script setup>
import { ref } from "vue";
const showInput = ref(false);
function toggleInput() {
showInput.value = !showInput.value;
if (showInput.value) {
const input = document.getElementById("input");
input?.focus();
}
}
</script>
<template>
<button @click="toggleInput">Mostrar input</button>
<input id="input" v-if="showInput" />
</template>
Quando toggleInput
modifica o valor de showInput
, o Vue agenda a função de template
para ser re-executada. Entretanto, quando tentamos selecionar o elemento,
o input
ainda não está no DOM.
Nesse caso, precisamos utilizar nextTick()
, onde o "tick" é a execução das funções de template
que vão atualizar o DOM. Você pode tanto passar um callback que será executado logo após, quanto aguardar
a Promise que ele retorna.
function toggleInput() {
showInput.value = !showInput.value;
if (showInput.value) {
nextTick(() => {
const input = document.getElementById("input");
input?.focus();
});
}
}
// OU
async function toggleInput() {
showInput.value = !showInput.value;
if (showInput.value) {
await nextTick();
const input = document.getElementById("input");
input?.focus();
}
}
Ordem dos efeitos
A ordem dessas execuções também é importante. Um watch
ou watchEffect
acontece sempre antes
do DOM ser atualizado. Portanto, mesmo utilizando nextTick
dentro de um watchEffect
,
você não tem acesso à versão atualizada do DOM.
<script setup>
import { nextTick } from "vue";
import { ref, watchEffect } from "vue";
const showInput = ref(false);
async function toggleInput() {
showInput.value = !showInput.value;
}
watchEffect(async () => {
if (showInput.value) {
const input = document.getElementById("input");
await nextTick();
// Não vai funcionar
input?.focus();
}
});
</script>
<template>
<button @click="toggleInput">Mostrar input</button>
<input v-if="showInput" id="input" />
</template>
O Vue fornece duas formas de contornar esse problema. Você pode passar a opção flush: 'post'
ou utilizar watchPostEffect
, que é uma versão "pré-configurada" do watchEffect
com essa opção já definida.
watchEffect(
() => {
if (showInput.value) {
const input = document.getElementById("input");
input?.focus();
}
},
{
flush: "post",
}
);
// ou
watchPostEffect(() => {
if (showInput.value) {
const input = document.getElementById("input");
input?.focus();
}
});
Efeitos síncronos
Outra possibilidade que o Vue oferece é executar os watchers imediatamente, utilizando a opção flush: 'sync'
ou watchSyncEffect
.
Voltando ao exemplo anterior:
watchEffect(
async () => {
console.log("count increased", count.value);
},
{
flush: "sync",
}
);
// ou
watchSyncEffect(async () => {
console.log("count increased", count.value);
});
Agora teremos um log para cada vez que o estado for modificado. É claro que é preciso muito cuidado, efeitos síncronos podem causar problemas de performance e loops infinitos se não usados corretamente.
Conclusão
Exceto nos casos em que você precisa acessar ou modificar o DOM, raramente precisa se preocupar com a ordem de efeitos ou quando essas mudanças vão acontecer. O Vue possui muitas abstrações e otimizações, e é importante não tentar otimizar antes de realmente ter um problema.