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. ;-)')
const loading = ref(false)
// watch agit directement sur une ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?')) {
loading.value = true
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
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</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 est égal à: ${count}`)
})
À la place, utilisez un accesseur :
js
// à la place, utilisez un accesseur :
watch(
() => obj.count,
(count) => {
console.log(`Count est égal à: ${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 }
)
In Vue 3.5+, the deep
option can also be a number indicating the max traversal depth - i.e. how many levels should Vue traverse an object's nested properties. À partir de la version 3.5, l'option deep
peut aussi être un numbre indiquant la profondeur maximale à traverser, c'est-à-dire de combien de niveaux Vue traverse un objet avec des propriétés imbriquées.
À 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) => {
// execution immédiate, puis à chaque fois que `source` change
},
{ immediate: true }
)
Observateurs unitaires
- Supporté à partir de la version 3.4
La fonction de rappel de l'observateur sera exécutée dès lors qu'une source observée change. Si vous voulez que la fonction de rappel soit déclenchée une seule fois quand il y a un changement, utilisez l'option once: true
.
js
watch(
source,
(newValue, oldValue) => {
// quand `source` change, déclenchée une seule fois
},
{ once: 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.
Side Effect Cleanup
Sometimes we may perform side effects, e.g. asynchronous requests, in a watcher:
js
watch(id, (newId) => {
fetch(`/api/${newId}`).then(() => {
// callback logic
})
})
But what if id
changes before the request completes? When the previous request completes, it will still fire the callback with an ID value that is already stale. Ideally, we want to be able to cancel the stale request when id
changes to a new value.
We can use the onWatcherCleanup()
API to register a cleanup function that will be called when the watcher is invalidated and is about to re-run:
js
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// callback logic
})
onWatcherCleanup(() => {
// abort stale request
controller.abort()
})
})
Note that onWatcherCleanup
is only supported in Vue 3.5+ and must be called during the synchronous execution of a watchEffect
effect function or watch
callback function: you cannot call it after an await
statement in an async function.
Alternatively, an onCleanup
function is also passed to watcher callbacks as the 3rd argument, and to the watchEffect
effect function as the first argument:
js
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
watchEffect((onCleanup) => {
// ...
onCleanup(() => {
// cleanup logic
})
})
This works in versions before 3.5. In addition, onCleanup
passed via function argument is bound to the watcher instance so it is not subject to the synchronously constraint of onWatcherCleanup
.
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.
Comme pour les mises à jour de composants, les rappels de l'observateur créés par l'utilisateur sont regroupés afin d'éviter les invocations en double. Par exemple, nous ne voulons probablement pas qu'un observateur se déclenche mille fois si nous introduisons de manière synchrone mille éléments dans un tableau observé.
Par défaut, le rappel d'un observateur est appelé après les mises à jour du composant parent (le cas échéant), et avant les mises à jour du DOM du composant propriétaire. Cela signifie que si vous tentez d'accéder au DOM du composant propriétaire à l'intérieur d'un callback de l'observateur, le DOM sera dans un état de pré-mise à jour.
Observateurs à posterio
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 */
})
Observateurs synchrones
Il est également possible de créer un observateur qui se déclenche de manière synchrone, avant toute mise à jour gérée par Vue.
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
Sync watchEffect()
a également un alias de commodité, watchSyncEffect()
:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* exécuté de manière synchrone lors d'une modification réactive des données */
})
A utiliser avec précaution
Les observateurs synchrones n'ont pas de fonction de mise en lot et se déclenchent à chaque fois qu'une mutation réactive est détectée. Il est possible de les utiliser pour surveiller de simples valeurs booléennes, mais il faut éviter de les utiliser sur des sources de données qui peuvent être mutées plusieurs fois de manière synchrone, par exemple des tableaux.
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
}
})