跪拜 Guibai
← Back to the summary

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

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

卷土的土

Deep watching is not the same as preserving an old snapshot.