对象原地修改后,watch 回调中的 newValue 与 oldValue 通常指向同一个代理对象。watch 只负责通知响应式变化,不会自动保存历史快照。要获得完整的新旧状态,首先要区分"整体替换对象"和"修改对象属性"。
行为模型
ref 是带 .value 的容器,reactive 直接返回 Proxy。watch(refObj) 默认只监听 .value 的整体替换;修改嵌套属性需要开启 { deep: true }。
直接监听 reactive 对象时会隐式深度监听,因此属性变化能触发回调,但新旧值仍是同一个对象。
ts
const aaa = ref({a: 1, b: 2})
const bbb = reactive({a: 1, b: 2})
watch(aaa, cb) // 监听 aaa.value 替换
watch(aaa, cb, { deep: true }) // 监听嵌套属性
watch(bbb, cb) // reactive 隐式深度监听
| 操作 | 是否触发 | 能否直接取得独立旧值 |
|---|---|---|
aaa.value = { ...aaa.value } |
是 | 能 |
aaa.value.a = 2 |
开启 deep 后能 |
不能 |
bbb.a = 2 |
能 | 不能 |
重新赋值 reactive 变量 |
不能 | 不能 |
获取reactive对象完整快照
扁平对象可用 computed 生成浅快照:
ts
const snapshot = computed(() => ({ ...bbb }))
watch(snapshot, (newValue, oldValue) => {
console.log(oldValue, newValue)
})
但浅拷贝只隔离第一层。对象包含嵌套结构时,需要深拷贝,或通过 watch 手动维护上一份快照。structuredClone 不能直接克隆 Proxy;在 computed 中使用 toRaw 又可能丢失依赖追踪,因此克隆方式必须同时保证依赖读取和数据类型安全。
ts
let previousSnapshot = structuredClone(toRaw(bbb))
watch(bbb, () => {
const currentSnapshot = structuredClone(toRaw(bbb))
console.log('变化前:', previousSnapshot)
console.log('变化后:', currentSnapshot)
previousSnapshot = currentSnapshot
})
结论
- 能监听具体字段时,优先监听字段,成本最低。
- 希望获得"整体替换"的新旧对象,使用
ref并整体赋值。 - 小对象可以借助
computed生成快照。 - 深层、复杂对象更适合
watch加手动深拷贝。