先记住这个答案
直接把 reactive 对象交给 watch,默认会观察其深层变化;把保存对象的 ref 交给 watch,通常先观察 ref 的 value 是否变化,内部字段变动需要 deep 或具体字段 getter。getter 返回对象时,默认也主要根据返回对象是否变化判断回调。它们可以指向相近数据,却表达不同的观察范围。应按照要监听嵌套修改还是整体替换来选择来源,而不是只看数据是一个对象。
- 直接 reactive 来源带有默认深层观察行为
- 对象 ref 的深响应式转换不等于 watch 默认深遍历
- 返回对象属性的 getter 可以保留对整体替换的观察
不要把响应式转换与监听遍历混在一起
ref({ settings: { level: 1 } }) 中的对象通常也具有深层响应式能力,组件直接读取 level 时可以参与追踪。但 watch 这个 ref 的默认入口并不因此自动等同于遍历整棵对象。只改 settings.level 与把 ref.value 换成新对象,是两种不同变化。
直接 watch(reactiveObject, callback) 则采用默认深层观察,因此内部字段修改也能引起回调。面试时可以先讲“数据能否产生响应式通知”,再讲“这个监听器订阅了哪些读取”,避免用一个深响应式概念解释所有来源形式。
监听当前子对象与跟随子对象替换不同
假设 state.profile 是一个响应式对象,watch(state.profile, callback) 观察的是调用时传入的那个对象。后来把 state.profile 替换为新对象,并不会让监听器凭空知道外层这个属性入口已经换了目标。它与 watch(() => state.profile, callback) 所表达的关系不同。
getter 每次从 state 读取 profile,可以观察这个属性指向的对象是否替换;如果还需要新对象内部变化,可以进一步使用 deep。需要只关心 profile.name 时,直接返回 name 的 getter 往往更准确,也避免观察头像加载状态等无关字段。
选择最小的完整观察范围
对于服务端返回整份不可变快照的结果,监听 ref 的整体替换可能已经足够。对于用户直接编辑的复杂对象,若副作用确实依赖任意字段,则可以采用深层观察,但要了解遍历成本与 oldValue 不会自动克隆的限制。两种模型都可以成立,关键是更新协议与监听方式一致。
验证时应至少覆盖嵌套字段修改、整个对象替换以及旧对象后续仍被修改三类操作。这样能发现监听还停留在旧对象、深度不够或观察范围过宽的问题。默认批量调度可能合并连续变化,因此测试预期也应围绕实际回调时机设计。
容易答错的地方
- ref 里的对象不能响应深层修改
- 普通 ref 通常会对对象进行深层响应式转换,只是 watch 该 ref 的默认观察范围不同。不能把监听器没有回调直接归因于对象完全不响应。
- 直接监听子对象会永远跟随父属性换新值
- 监听器拿到的是当时对象,父属性的替换关系需要通过 getter 或稳定容器表达。持有旧对象的引用不会自动变成新对象。
面试官还会怎么问?
怎样同时观察对象替换和内部字段修改?
可以监听返回该对象属性的 getter,并按需要开启 deep;或者更准确地监听具体字段。应确认深度和数据规模,而不是默认对所有对象全面遍历。
直接 watch reactive 对象时能拿到独立旧快照吗?
不能由默认深监听推导有历史快照。内部原地修改时新旧参数可能是同一个对象,需要独立设计比较或历史记录。
使用 shallowRef 后 deep 能自动补回所有能力吗?
不能把 deep 当成把任意普通内部对象变成深响应式的开关。浅层容器有明确的通知边界,内部非响应式写入不会凭空产生通知,通常应按整体替换等约定更新。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。