Vue 3 watch's oldValue and newValue Point to the Same Proxy: Snapshot Strategies for ref and reactive
Vue 3 Object Listening and Snapshots: Key Differences Between ref and reactive
After an object is modified in place, the newValue and oldValue in a watch callback usually point to the same proxy object. watch is only responsible for notifying reactive changes; it does not automatically save historical snapshots. To get a complete old and new state, first distinguish between replacing the whole object and modifying an object property.
Behavior Model
ref is a container with .value, while reactive directly returns a Proxy. watch(refObj) by default only listens for replacement of .value; modifying nested properties requires { deep: true }.
When you directly watch a reactive object, a deep watch is implicitly enabled, so property changes can trigger the callback, but the old and new values are still the same object.
const aaa = ref({ a: 1, b: 2 })
const bbb = reactive({ a: 1, b: 2 })
watch(aaa, cb) // watches replacement of aaa.value
watch(aaa, cb, { deep: true }) // watches nested properties
watch(bbb, cb) // reactive implicitly deep-watches
| Operation | Triggers? | Can you directly get an independent old value? |
|---|---|---|
aaa.value = { ...aaa.value } |
Yes | Yes |
aaa.value.a = 2 |
Only with deep enabled |
No |
bbb.a = 2 |
Yes | No |
Reassigning the reactive variable |
No | No |
Getting a complete snapshot of a reactive object
For flat objects, computed can generate a shallow snapshot:
const snapshot = computed(() => ({ ...bbb }))
watch(snapshot, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
But a shallow copy only isolates the first layer. If the object contains nested structures, you need a deep copy, or manually maintain the previous snapshot through watch. structuredClone cannot directly clone a Proxy; using toRaw inside computed may lose dependency tracking, so the cloning approach must guarantee both dependency reading and data type safety.
let previousSnapshot = structuredClone(toRaw(bbb))
watch(bbb, () => {
const currentSnapshot = structuredClone(toRaw(bbb))
console.log('Before change:', previousSnapshot)
console.log('After change:', currentSnapshot)
previousSnapshot = currentSnapshot
})
Conclusion
- When a specific field can be watched, prefer watching the field; it has the lowest cost.
- To get old and new objects for whole replacement, use
refand assign the whole object. - For small
reactiveobjects, usecomputedto generate a snapshot. - For deep, complex
reactiveobjects,watchplus manual deep copy is more suitable.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Deep watching is not the same as preserving an old snapshot.