先记住这个答案
对象内部字段改变时,外层对象没有被替换。深度 watch 能因嵌套变化触发回调,但 newValue 与 oldValue 可能仍指向同一响应式对象,所以两边读取字段时都看到修改后的内容。这不代表监听没生效,而是引用不等于历史快照。只关心少量字段时可以监听返回原始值的 getter;需要完整差异时,应提前维护适合数据类型的快照或记录明确的业务更新动作。
- 回调触发与对象引用是否替换是两个问题
- oldValue 不是深度历史快照的保证
- 想得到旧字段值,应在它被覆盖前保存或监听该值
同一个对象变化后,两条引用看到同一份内容
假设 state.preferences.level 从一变成二,state 仍是原来的代理对象。深监听回调中的两个参数都指向它,于是 oldValue.preferences.level 也读取到二。Vue 已经观察到了变化,否则回调不会触发,但它没有为每个监听器自动保存修改前的整棵树。
下面同时监听整个对象和一个返回原始数值的 getter。对象回调显示引用相同、两边字段都为二;字段回调则有二和一这两个独立数值。示例使用同步 flush 方便观察一条赋值的结果,业务中不应为了得到日志顺序而对高频大对象一律使用同步回调。
import { reactive, watch } from 'vue';
const state = reactive({ preferences: { level: 1 } });
const objects: Array<{ same: boolean; before: number; after: number }> = [];
const fields: number[][] = [];
const stopObject = watch(state, (next, previous) => {
objects.push({ same: next === previous, before: previous.preferences.level, after: next.preferences.level });
}, { flush: 'sync' });
const stopField = watch(() => state.preferences.level, (next, previous) => {
fields.push([next, previous]);
}, { flush: 'sync' });
state.preferences.level = 2;
stopObject();
stopField();对象记录为 same=true、before=2、after=2,字段记录为 [2, 1]。两种监听都触发了,只是保存的来源结果不同;示例没有固定控制台输出。
需要完整差异时,快照必须有正确时机
如果要提交修改差异,可以先建立一份已确认版本快照,再在变更后比较当前值,并按业务规则推进基准版本。快照应在旧内容尚存在时生成,回调里第一次看到 oldValue 才开始复制,往往已经太晚。保存成功后是否更新基准,还要考虑同时发生的新编辑。
复制方式也依赖数据类型。JSON 方式不能完整保留所有类型,原始响应式代理也不能直接假定适用于每一种克隆 API。应定义可序列化的数据边界与复制规则,避免把函数、外部实例或循环关系混进简单表单快照,然后以克隆失败为由放弃历史一致性。
撤销与审计不一定需要每次深拷贝
复杂编辑器可以记录语义化动作、不可变版本或增量补丁,而不是每次按键都复制全部状态。这些方案需要明确动作是否可逆、如何合并连续操作,以及外部数据更新与本地草稿的冲突处理。watch 的新旧参数不能自动提供这些业务语义。
验收时可以分别测试嵌套修改和整个对象替换,确认差异逻辑是否处理两种情况。默认调度可能合并同一轮同步变化,所以回调也不等于每个底层赋值的完整事件日志。需要逐动作审计时,应在明确的更新入口记录,而不是依赖观察器猜测用户做了什么。
容易答错的地方
- newValue 等于 oldValue 就说明这次回调没有意义
- 引用相同仍可能包含已经发生的嵌套变化。需要根据监听目标决定回调职责,而不是把对象身份比较当作所有内容变化的判断方式。
- 在回调里 clone(oldValue) 就能拿到修改前状态
- 如果旧参数指向已被修改的同一个对象,克隆得到的也是当前内容。历史数据必须在覆盖前保存,或者通过明确的不可变更新与动作日志保留。
面试官还会怎么问?
整个对象替换后,新旧参数还会相同吗?
通过能观察替换的来源,例如 ref 或返回对象属性的 getter,替换为新对象时通常可以得到不同引用。直接监听旧对象的入口则不等于持续跟随外层变量替换。
监听 getter 返回一个新数组能获得字段旧值吗?
如果数组项是读取到的原始值,可以保留这些值在上一轮的组合结果。但嵌套对象仍可能共享引用,不能把外层新数组当成自动深拷贝。
深监听适合精确记录每次赋值吗?
默认调度可能合并同步变化,且回调没有天然的业务动作含义。需要逐操作历史时,使用明确的更新入口和记录协议更可靠。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。