Observateurs
Exemple Basique
Les propriétés calculées nous permettent de calculer des valeurs dérivées de manière déclarative. Toutefois, il y a des cas dans lesquels nous devons réaliser des "effets de bord" en réaction aux changements de l'état - par exemple, muter le DOM, ou changer une autre partie de l'état en fonction du résultat d'une opération asynchrone.
Avec la Composition API, nous pouvons utiliser la fonction watch
pour déclencher une fonction de rappel chaque fois qu'une partie d'un état réactif change :
vue
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// watch agit directement sur une ref
watch(question, async (newQuestion) => {
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>
Les types de sources de watch
Le premier argument de watch
peut être différents types de "sources" réactives : ça peut être une ref (y compris des refs calculées), un objet réactif, une fonction accesseur, ou un tableau de différentes sources :
js
const x = ref(0)
const y = ref(0)
// simple ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// accesseur
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// tableau de différentes sources
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
Notez que vous ne pouvez pas observer une propriété d'un objet réactif de cette manière :
js
const obj = reactive({ count: 0 })
// cela ne fonctionnera pas car on passe un nombre à watch()
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
À la place, utilisez un accesseur :
js
// à la place, utilisez un accesseur :
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
Observateurs profonds
Lorsque vous appelez watch()
directement sur un objet réactif, un observateur profond va implicitement être créé - la fonction de rappel sera déclenchée à chaque mutation imbriquée :
js
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// exécution à chaque mutation d'une propriété imbriquée
// Remarque : `newValue` sera égale à `oldValue` ici
// car elles pointent toutes les deux sur le même objet !
})
obj.count++
Il ne faut pas confondre avec un accesseur qui retourne un objet réactif - dans ce dernier cas, la fonction de rappel ne sera exécutée que si l'accesseur retourne un objet différent :
js
watch(
() => state.someObject,
() => {
// exécution seulement lorsque state.someObject est remplacé
}
)
Toutefois, vous pouvez transformer ce second cas en un observateur profond en utilisant explicitement l'option deep
:
js
watch(
() => state.someObject,
(newValue, oldValue) => {
// Remarque : `newValue` sera égale à `oldValue` ici
// *sauf si* state.someObject a été remplacé
},
{ deep: true }
)
À utiliser avec précaution
Les observateurs profonds nécessitent de traverser toutes les propriétés imbriquées de l'objet observé, et peuvent être consommateur de ressources lorsqu'ils sont utilisés sur des structures importantes de données. Utilisez les seulement si nécessaire, en ayant conscience des implications en matière de performances.
Les observateurs impatients
watch
fonctionne à la volée par défaut : la fonction de rappel ne sera pas appelée tant que la source observée n'aura pas changé. Mais dans certains cas, on peut souhaiter que cette même logique de rappel soit exécutée de manière précoce - par exemple, on peut vouloir récupérer des données initiales, puis les récupérer de nouveau chaque fois qu'un état pertinent change.
Nous pouvons forcer l'exécution immédiate d'un observateur en passant l'option immediate: true
:
js
watch(source, (newValue, oldValue) => {
// exécuté immédiatement, à nouveau quand la `source` changera
}, { immediate: true })
watchEffect()
Il est commun pour la fonction de l'observateur d'utiliser exactement le même état réactif comme source. Par exemple, considérez le code suivant, qui utilise un observateur pour charger une ressource distante à chaque changement de la ref todoId
:
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 })
En particulier, remarquez comment l'observateur utilise doublement todoId
, une fois comme source, ensuite à nouveau à l'intérieur de la fonction.
Cela peut être simplifié par watchEffect()
. watchEffect()
nous permet d'effectuer des effets de bord immédiatement tout en traquant automatiquement les dépendances réactives de cet effet. L'exemple précédent peut être réécrit de la sorte :
js
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
Ici, la fonction sera exécutée immédiatement, il n'y a pas besoin de spécifier immediate : true
. Pendant son exécution, elle suivra automatiquement todoId.value
comme une dépendance (similaire aux propriétés calculées). Chaque fois que todoId.value
change, la fonction sera exécutée à nouveau. Avec watchEffect()
, nous n'avons plus besoin de passer explicitement todoId
comme source.
Vous pouvez vous référer à cet exemple avec watchEffect
et une récupération de données en action.
Pour des exemples comme ceux-ci, avec une seule dépendance, le bénéfice de watchEffect()
est relativement faible. Mais pour les surveillances qui ont plusieurs dépendances, l'utilisation de watchEffect()
supprime la charge de maintenir la liste des dépendances manuellement. De plus, si vous devez surveiller plusieurs propriétés dans une structure de données imbriquée, watchEffect()
peut s'avérer plus efficace qu'un observateur profond, car il ne suivra que les propriétés qui sont utilisées dans la fonction, plutôt que de les suivre toutes de manière récursive.
TIP
watchEffect
traque les dépendances seulement pendant son exécution synchrone. Lorsque vous l'utilisez avec un rappel asynchrone, seules les propriétés accédées avant le premier événement await
seront traquées.
watch
vs. watchEffect
watch
et watchEffect
permettent tous les deux de réaliser des effets de bord. Leur principale différence réside dans la manière dont ils traquent leurs dépendances réactives :
watch
traque seulement la source explicitement observée . De plus, le rappel n'est déclenché que lorsque la source a bien changé.watch
sépare la traque des dépendances et les effets de bord, ce qui nous donne plus de contrôle sur le moment où le rappel doit être exécuté.watchEffect
, d'un autre côté, combine la traque des dépendances et les effets de bord en une phase. Il traque automatiquement chaque propriété réactive accédée durant son exécution synchrone. Cela est plus pratique et rend généralement le code plus concis, mais rend les dépendances réactives moins explicites.
Timing de nettoyage des rappels
Lorsque vous mutez un état réactif, cela peut déclencher à la fois la mise à jour des composants Vue et des rappels d'observateur que vous avez créés.
Par défaut, les rappels des observateurs créés par les utilisateurs sont appelés avant la mise à jour des composants Vue. Cela signifie que si vous essayez d'accéder au DOM pendant le rappel d'un observateur, le DOM sera dans l'état d'avant la mise à jour de Vue.
Si vous voulez accéder au DOM après que Vue l'ait mis à jour, vous devez spécifier l'option flush: 'post'
:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
Le watchEffect()
"post-flush" a également un pseudonyme de confort, watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* exécution après la mise à jour de Vue */
})
Arrêter un observateur
Les observateurs déclarés de manière synchrone à l'intérieur de setup()
ou <script setup>
sont liés à l'instance du composant propriétaire, et seront automatiquement arrêtés lorsque ce dernier sera démonté. Dans la plupart des cas, vous n'avez pas à vous soucier d'arrêter les observateurs vous-même.
La clé ici est que l'observateur doit être créé de manière synchrone : si l'observateur est créé dans un rappel asynchrone, il ne sera pas lié au composant propriétaire et devra être arrêté manuellement afin d'éviter des fuites de mémoire. Voici un exemple :
vue
<script setup>
import { watchEffect } from 'vue'
// celui-ci sera automatiquement arrêté
watchEffect(() => {})
// ...celui-là non!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
Pour arrêter manuellement un observateur, utilisez la fonction de gestion qu'il retourne. Cela fonctionne pour watch
et pour watchEffect
:
js
const unwatch = watchEffect(() => {})
// ...plus tard, lorsqu'il n'est plus nécessaire
unwatch()
Notez que les cas où vous devriez être amenés à créer des observateurs de manière asynchrone sont rares, et une création synchrone devrait être choisie lorsque c'est possible. Si vous devez attendre des données asynchrones, vous pouvez intégrer votre logique d'observation dans une condition :
js
// données à récupérer de manière asynchrone
const data = ref(null)
watchEffect(() => {
if (data.value) {
// on fait quelque chose lorsque les données sont chargées
}
})