Vue 3 对象监听与快照:ref 和 reactive 的关键差异

238 阅读1分钟

对象原地修改后,watch 回调中的 newValue 与 oldValue 通常指向同一个代理对象。watch 只负责通知响应式变化,不会自动保存历史快照。要获得完整的新旧状态,首先要区分“整体替换对象”和“修改对象属性”。

行为模型

ref 是带 .value 的容器,reactive 直接返回 Proxy。watch(refObj) 默认只监听 .value 的整体替换;修改嵌套属性需要开启 { deep: true }。

直接监听 reactive 对象时会隐式深度监听,因此属性变化能触发回调,但新旧值仍是同一个对象。

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 生成浅快照:

const snapshot = computed(() => ({ ...bbb }))
watch(snapshot, (newValue, oldValue) => {
  console.log(oldValue, newValue)
})

但浅拷贝只隔离第一层。对象包含嵌套结构时,需要深拷贝,或通过 watch 手动维护上一份快照。structuredClone 不能直接克隆 Proxy;在 computed 中使用 toRaw 又可能丢失依赖追踪,因此克隆方式必须同时保证依赖读取和数据类型安全。

let previousSnapshot = structuredClone(toRaw(bbb))

watch(bbb, () => {
  const currentSnapshot = structuredClone(toRaw(bbb))

  console.log('变化前:', previousSnapshot)
  console.log('变化后:', currentSnapshot)

  previousSnapshot = currentSnapshot
})

结论

  • 能监听具体字段时,优先监听字段,成本最低。
  • 希望获得“整体替换”的新旧对象,使用 ref 并整体赋值。
  • 小对象(reactive)可以借助 computed 生成快照。
  • 深层、复杂对象(reactive)更适合 watch 加手动深拷贝。