I Watcher
Esempio Base
Le computed properties ci permettono di calcolare in maniera dichiarativa dei valori derivati. Tuttavia, ci sono casi in cui abbiamo bisogno di eseguire "side effects" in risposta ai cambiamenti di stato - ad esempio: fare modifiche al DOM o cambiare un altro pezzo di stato in base al risultato di un'operazione asincrona.
Con la Composition API possiamo utilizzare la funzione watch
per innescare una callback ogni volta che cambia uno stato reattivo:
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Le domande contengono di solito un punto interrogativo. ;-)')
// watch funziona direttamente su un ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Sto pensando...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Errore! Impossibile raggiungere l\'API. ' + error
}
}
})
</script>
<template>
<p>
Fai una domanda a risposta sì/no:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
Tipi di Valori Osservati
Il primo argomento di watch
può avere diversi tipi di "sorgenti" reattive: può essere un ref (inclusi i ref calcolati), un oggetto reattivo, una funzione getter o un array di valori multipli:
js
const x = ref(0)
const y = ref(0)
// ref singola
watch(x, (newX) => {
console.log(`x è ${newX}`)
})
// getter
watch(
() => x.value + y.value,
(sum) => {
console.log(`la somma di x + y è: ${sum}`)
}
)
// array di valori multipli
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x è ${newX} e y è ${newY}`)
})
Nota che non puoi osservare una proprietà di un oggetto reattivo in questo modo:
js
const obj = reactive({ count: 0 })
// questo non funzionerà perché stiamo passando un numero a watch()
watch(obj.count, (count) => {
console.log(`count è: ${count}`)
})
Invece, utilizza un getter:
js
// instead, use a getter:
watch(
() => obj.count,
(count) => {
console.log(`count è: ${count}`)
}
)
Watcher Avanzati
Quando chiami watch()
direttamente su un oggetto reattivo, verrà creato implicitamente un watcher avanzato - la callback verrà attivata su tutte le mutazioni annidate:
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// si attiva con mutazioni di proprietà annidate
// Nota: qui newValue sarà uguale a oldValue
// perché entrambi puntano allo stesso oggetto!
})
obj.count++
Questo dovrebbe essere differenziato da un getter che restituisce un oggetto reattivo - nel secondo caso, la callback verrà attivata solo se il getter restituisce un oggetto diverso:
js
watch(
() => state.someObject,
() => {
// si attiva solo quando state.someObject viene sostituito
}
)
Puoi forzare, tuttavia, il secondo caso in un watcher avanzato utilizzando esplicitamente l'opzione deep
:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Nota: qui newValue sarà uguale a oldValue
// *a meno che* state.someObject non sia stato sostituito
},
{ deep: true }
)
Usa con Cautela
Il watcher avanzato richiede il traversing di tutte le proprietà annidate nell'oggetto osservato e può essere costoso se utilizzato su grandi strutture dati. Usalo solo quando necessario e tieni conto delle implicazioni sulle prestazioni.
Watcher Immediati
Di default watch
è "pigro": la callback non verrà chiamata finché la fonte osservata non cambierà. Ma in alcuni casi potremmo volere che la stessa logica di callback venga eseguita immediatamente - ad esempio, potremmo voler recuperare alcuni dati iniziali e poi ri-recuperare i dati ogni volta che ci siano modifiche dello stato.
Possiamo forzare l'esecuzione immediata della callback di un watcher passando l'opzione immediate: true
:
js
watch(source, (newValue, oldValue) => {
// eseguito immediatamente, poi di nuovo quando `source` cambia
}, { immediate: true })
watchEffect()
È comune che la callback del watcher utilizzi esattamente lo stesso stato reattivo del valore osservato. Ad esempio, considera il seguente codice, che utilizza un watcher per caricare una risorsa remota ogni volta che la ref todoId
cambia:
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 })
In particolare notiamo come il watcher utilizzi todoId
due volte, una volta come valore osservato e poi di nuovo all'interno della callback.
Questo può essere semplificato con watchEffect()
. watchEffect()
ci permette di tenere traccia automaticamente delle dipendenze reattive della callback. Il watcher sopra può essere riscritto come:
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
Qui, la callback verrà eseguita immediatamente, senza bisogno di specificare immediate: true
. Durante la sua esecuzione, terrà automaticamente traccia di todoId.value
come dipendenza (come nelle computed properties). Ogni volta che todoId.value
cambia, la callback verrà eseguita di nuovo. Con watchEffect()
non c'è più bisogno di passare todoId
esplicitamente come valore osservato.
Puoi vedere questo esempio di watchEffect()
e del recupero di dati reattivi in azione.
Per questo tipo di esempi, con una sola dipendenza, il vantaggio di watchEffect()
è relativamente piccolo. Ma per i watcher che hanno molteplici dipendenze, l'utilizzo di watchEffect()
elimina la responsabilità di dover mantenere manualmente l'elenco delle dipendenze. Inoltre, se hai bisogno di osservare diverse proprietà in una struttura di dati nidificata, watchEffect()
può rivelarsi più efficiente di un watcher avanzato, poiché terrà traccia solo delle proprietà utilizzate nella callback, invece di tracciarle tutte in modo ricorsivo.
TIP
watchEffect
monitora le dipendenze solo durante l'esecuzione che avviene in modo sincrono. Se lo si utilizza con una callback asincrona, solo le proprietà a cui si accede prima del primo comando await
vengono considerate come dipendenze.
watch
vs. watchEffect
watch
e watchEffect
permettono entrambi di eseguire "side effects" in modo reattivo. La loro differenza principale sta nel modo in cui tracciano le dipendenze reattive:
watch
tiene traccia soltanto della fonte specificata che sta osservando. Non tiene conto di ciò che viene utilizzato all'interno della callback. Inoltre, la callback viene innescata solo quando la fonte specifica cambia effettivamente.watch
separa il monitoraggio delle dipendenze dall'azione che deve essere eseguita, offrendo un controllo più dettagliato sul "quando" la callback deve essere attivata.watchEffect
, invece, unisce il monitoraggio delle dipendenze con l'azione in una sola fase. Esso traccia automaticamente ogni proprietà reattiva che viene utilizzata durante la sua esecuzione sincrona. Questo approccio è più pratico e generalmente porta a un codice più snello, ma rende meno chiare le dipendenze reattive.
Tempi di esecuzione della Callback
La modifica di uno stato reattivo potrebbe innescare sia gli aggiornamenti del componente Vue sia le callback del watcher da te create.
Di default, le callback del watcher create dall'utente vengono chiamate prima degli aggiornamenti del componente Vue. Questo significa che se tenti di accedere al DOM all'interno di una callback del watcher, il DOM sarà nello stato precedente all'applicazione degli aggiornamenti da parte di Vue.
Se desideri accedere al DOM in una callback del watcher dopo che Vue lo ha aggiornato, devi specificare l'opzione flush: 'post'
:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
L'opzione post-flush di watchEffect()
ha anche un comodo alias, watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* eseguito dopo gli aggiornamenti di Vue */
})
Fermare un Watcher
I watcher dichiarati in modo sincrono all'interno di setup()
o <script setup>
sono legati all'istanza del componente proprietario e verranno fermati automaticamente quando il componente proprietario passa nello stage unmounted
(smontato). Nella maggior parte dei casi, quindi, non devi preoccuparti di fermare tu stesso il watcher.
Il concetto chiave qui è che il watcher deve essere creato in modo sincrono: se il watcher viene creato in una callback asincrona, non sarà legato al componente proprietario e dovrà essere fermato manualmente per evitare perdite di memoria. Ecco un esempio:
vue
<script setup>
import { watchEffect } from 'vue'
// questo verrà fermato automaticamente
watchEffect(() => {})
// ...questo no!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
Per fermare manualmente un watcher, utilizza la funzione handle. Questo vale sia per watch
che per watchEffect
:
js
const unwatch = watchEffect(() => {})
// ... più tardi, quando non è più necessario
unwatch()
Nota che dovrebbero esserci pochissimi casi in cui hai bisogno di creare watcher in modo asincrono, e che la creazione sincrona dovrebbe essere preferita ogni volta che è possibile. Se hai bisogno di attendere alcuni dati asincroni, puoi usare una logica condizionale per il tuo watcher:
js
// dati da caricare in modo asincrono
const data = ref(null)
watchEffect(() => {
if (data.value) {
// fai qualcosa quando i dati sono caricati
}
})