Listener Watch
In modern Vue application development, managing reactive state changes often requires executing "side effects" such as triggering asynchronous network requests, directly interacting with the DOM, or synchronizing data with local storage. While computed properties handle most derived data scenarios gracefully, the watcher (watch) serves as the ideal tool whenever you need to perform asynchronous or heavy computational tasks in response to data fluctuations.
A watcher is a reactive data monitoring mechanism provided by Vue. Its core philosophy revolves around registering a callback function for specific reactive sources; whenever those sources change, Vue automatically executes the callback while passing the newly updated value and the previous value as parameters.
Typical use cases for watchers include:
- Executing asynchronous operations following data changes (such as debounced search queries)
- Performing expensive or time-consuming tasks based on state transitions
- Comparing and evaluating differences between old and new values
- Tracking route mutations, form inputs, or global state transitions
In the Composition API, you can establish a watcher by importing thewatchfunction fromvue:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
</script>
<template>
<button @click="count++">Clicked {{ count }} times</button>
</template>
The callback function accepts two primary arguments: the new value followed by the old value. In the Options API, watchers are declared directly inside the watch option object:
export default {
data() {
return { count: 0 }
},
watch: {
count(newVal, oldVal) {
console.log(newVal, oldVal)
}
}
}
Watching Properties of Reactive Objects
When attempting to watch a specific property inside a reactive object, passing the property value directly will not work because primitive types are passed by value, failing to establish a reactive link. The correct approach is to supply a getter function as the data source:
import { reactive, watch } from 'vue'
const user = reactive({ name: 'John', age: 18 })
// Correct: Using a getter function
watch(() => user.name, (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`)
})
// Incorrect: watch(user.name, ...) will not trigger properly
Deep Watchers
When tracking nested properties inside an object, a standard getter function only detects changes at the reference level. To capture mutations deep within nested structures, you can enable the deep option:
const state = reactive({
profile: { city: 'Beijing' }
})
watch(
() => state.profile,
(newVal) => {
console.log('Profile updated:', newVal.city)
},
{ deep: true }
)
Keep two crucial considerations in mind:
- Passing a
reactiveobject directly intowatchimplicitly enables deep watching, where both the new and old values point to the same object reference. - Deep watching requires traversing all nested properties, which can introduce performance overhead on large data structures and should be used judiciously.
Common Options
Immediate Execution with immediate
By default, watcher callbacks are lazy—they only trigger after the monitored source changes. If you need the callback to run immediately upon component creation, set immediate: true:
watch(
keyword,
(newVal) => {
fetchSearchResult(newVal)
},
{ immediate: true }
)
This proves exceptionally useful in scenarios requiring initial data fetching on load, followed by subsequent re-fetches upon user input.
Watching Multiple Sources
The first argument of watch can also accept an array, allowing you to monitor multiple reactive sources simultaneously:
const firstName = ref('Doe')
const lastName = ref('John')
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Full name updated`)
}
)
Comparing watch and watchEffect
Vue 3 also provides watchEffect, which differs from watch in several key aspects:
- Dependency Tracking:
watchrequires explicitly declared data sources;watchEffectautomatically tracks every reactive dependency accessed within its callback. - Execution Timing:
watchis lazy by default;watchEffectexecutes immediately upon creation. - Value Access:
watchprovides access to both new and old values;watchEffectdoes not provide old values.
watchEffect(() => {
// Automatically tracks count.value and runs immediately
console.log('Current count:', count.value)
})
Generally speaking, use watch when you need old values or precise multi-source control, and opt for watchEffect when dealing with straightforward dependencies where automatic collection is preferred.
Stopping Watchers and Best Practices
Watchers created synchronously inside <script setup> automatically stop when the host component unmounts. However, if a watcher is initialized inside an asynchronous callback (such as a setTimeout), it must be stopped manually:
const stop = watch(count, callback)
// Manually stop the watcher when no longer needed
stop()
Best Practice Summary
- Prefer
computedproperties for derived data, reservingwatchstrictly for side effects. - Avoid overusing
deep: trueto prevent unnecessary performance degradation. - Always use a getter function when watching properties of
reactiveobjects. - Remember to manually stop watchers created within asynchronous workflows to prevent memory leaks.