Observadores
Exemplo Básico
As propriedades computadas permite-nos calcular declarativamente valores derivados. No entanto, existêm casos onde precisamos realizar "efeitos colaterais" em reação as mudanças de estado - por exemplo, alterando o DOM, ou mudando um outro pedaço do estado baseado no resultado de uma operação assíncrona.
Com a API de Composição, podemos utilizar a função watch
para acionar uma resposta sempre que um pedaço do estado reativo mudar:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// `watch` trabalha diretamente sobre uma referência
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
Experimente-o na Zona de Testes
Observar Tipos de Fonte
O primeiro argumento do watch
pode ser de diferentes tipos de "fontes" reativas: pode ser uma referência (incluindo referências computadas), um objeto reativo, uma função recuperada, ou um arranjo de várias fontes:
js
const x = ref(0)
const y = ref(0)
// referência única
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// recuperador
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// arranjo de várias fontes
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
Tome nota de que não podes observar uma propriedade de um objeto reativo desta maneira:
js
const obj = reactive({ count: 0 })
// isto não funcionará porque estamos passando um número para `watch()`
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
Ao invés daquilo, utilize um recuperador:
js
// Ao invés daquilo, utilize um recuperador:
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
Observadores Profundos
Quando chamares watch()
diretamente sobre um objeto reativo, ela criará implicitamente um observador profundo - a resposta será acionada sobre todas as mutações encaixadas:
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// dispara sobre mutações de propriedade encaixada
// Nota: cá `newValue` será igual ao `oldValue`
// porque ambos eles apontam para o mesmo objeto!
})
obj.count++
Isto deve ser distinguido de um recuperador que retorna um objeto reativo - no recente caso, a resposta só disparará se o recuperador retornar um objeto diferente:
js
watch(
() => state.someObject,
() => {
// dispara só quando `state.someObject` for substituido
}
)
Tu podes, no entanto, forçar o segundo caso para um observador profundo utilizando explicitamente a opção deep
:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Nota: cá `newValue` será igual ao `oldValue`
// *a menos que* `state.someObject` tenha sido substituido
},
{ deep: true }
)
USE COM CAUTELA
A observação profunda precisa percorrer todas propriedades encaixadas dentro do objeto observado, e pode ser caro quando utilizada sobre grandes estruturas de dados. Utilize-a só quando necessário e esteja ciente das implicações de desempenho.
Observadores Incansáveis
O watch
é preguiçoso por padrão: a resposta não será chamada até que a fonte observada tenha mudado. Mas em alguns casos podemos querer que a mesma lógica de resposta seja executada incansavelmente - por exemplo, podemos querer pedir alguns dados iniciais, e depois pedir novamente os dados sempre que o estado relevante mudar.
Nós podemos forçar uma resposta do observador a ser executada imediatamente passando a opção immediate: true
:
js
watch(source, (newValue, oldValue) => {
// executado imediatamente, depois novamente quando `source` mudar
}, { immediate: true })
watchEffect()
É comum para a função de resposta do observador usar exatamente o mesmo estado reativo como fonte. Por exemplo, considere o seguinte código, que usa um observador para carregar um recurso remoto sempre que a referência todoId
mudar:
js
const todoId = ref(1)
const data = ref(null)
watch(todoId, async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
}, { immediate: true })
Em particular, repare em como o observador usa o todoId
duas vezes, uma vez como fonte e depois novamente dentro da função de resposta.
Isto pode ser simplificado com watchEffect()
. A watchEffect()
permite-nos rastrear as dependências reativas da função de resposta automaticamente. O observador acima pode ser reescrito como:
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
Aqui, a função de resposta executará imediatamente, não há necessidade de especificar immediate: true
. Durante a sua execução, ela rastreará automaticamente o todoId.value
como uma dependência (similar as propriedades computadas). Sempre que todoId.value
mudar, a função de resposta será executada novamente. Com a watchEffect()
, já não precisamos passar todoId
explicitamente como valor de fonte.
Tu podes consultar este exemplo de watchEffect
e da requisição reativa de dados em ação.
Para exemplos como este, com apenas uma dependência, o benefício da watchEffect()
é relativamente pequeno. Mas para os observadores que têm várias dependências, usar watchEffect()
remove o fardo de ter que manter a lista de dependências manualmente. Além disto, se precisares observar várias propriedades em uma estrutura encaixada, a watchEffect()
pode provar-se mais eficiente do que um observador profundo, já que ele apenas rastreará as propriedades que são usadas na função de resposta, em vez de rastrear recursivamente todos eles.
DICA
A watchEffect
só rastreia dependências durante sua execução síncrona. Quando estiveres utilizando-a com uma resposta assíncrona, apenas as propriedades acessadas antes do primeiro visto await
serão executadas.
watch
vs. watchEffect
Ambos watch
e watchEffect
permitem-nos realizar efeitos colaterais de maneira reativa. A principal diferença entre elas está na maneira de como elas rasteiam as dependências reativas:
watch
só rastreia a fonte observada explicitamente. Ela não rastreiará nada acessado dentro da resposta. Além disto, a resposta só aciona quando a fonte tiver sido de fato mudada.watch
separa rastreiamente de dependência do efeito colateral, dando-nos controlo mais preciso sobre quando a resposta deveria disparar.watchEffect
, por outro lado, combina o rastreiamente de dependência e efeito colateral em uma fase. Ela rastreia automaticamente toda propriedade reativa acessada durante sua execução síncrona. Isto é mais conveniente e normalmente resulta em um código mais conciso, mas torna as suas dependências reativas menos explícitas.
Tempo de Fluxo de Resposta
Quando alterares o estado reativo, ele pode acionar tanto as atualizações de componente de Vue e respostas de observador criadados por ti.
Por padrão, respostas de observador criadas pelo utilizador são chamadas antes das atualização de componente de Vue. Isto significa que se tentares acessar o DOM de dentro de uma resposta de observador, o DOM estará no estado antes da Vue tiver aplicado quaisquer atualizações.
Se quiseres acessar o DOM em uma resposta de observador depois da Vue tiver atualizado-o, precisas especificar a opção flush: 'post'
:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
A watchEffect()
pós-fluxo também tem um pseudónimo de conveniência, watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* executada depois das atualizações de Vue */
})
Parando um Observador
Os observadores declarados sincronamente dentro de setup()
ou <script setup>
estão vinculados a instância do componente proprietário, e serão paradas automaticamente quando o componente proprietário for desmontado. Na maioria dos casos, não precisas te preocupares acerca de parar o observador por ti mesmo.
A chave aqui é que o observador deve ser criado sincronamente: se o observador for criado em uma resposta assíncrona, ele não estará vinculado ao componente proprietário e deve ser parado manualmente para evitar fugas de memória. Cá está um exemplo:
vue
<script setup>
import { watchEffect } from 'vue'
// este aqui será parado automaticamente
watchEffect(() => {})
// ...este aqui não será!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
Para parar manualmente um observado, utilize a função retornada para lidar com isto. Isto funciona para ambos watch
e watchEffect
:
js
const unwatch = watchEffect(() => {})
// ...mais tarde, quando for mais necessária
unwatch()
Nota que deve haver muito poucos casos onde precisas criar observadores assincronamente, e criação síncrona deve ser a preferida sempre que possível. Se precisares esperar por algum dado assíncrono, podes tornar a tua lógica de observação condicional:
js
// dados a serem carregados assincronamente
const data = ref(null)
watchEffect(() => {
if (data.value) {
// faça algo quando os dados forem carregados
}
})