先记住这个答案
props 中的嵌套原始字段应通过 getter 提供给 watch,例如 () => props.user?.id,直接传 props.user.id 得到的只是当前值。若嵌套值本身是响应式对象,直接监听它可以观察那个对象,但不等于会跟随父层把 user 换成新对象。需要同时处理替换与内部变化时,应通过 getter 表达入口,并按需决定深度。watch 只负责观察,不能让子组件获得修改只读 prop 的权限。
- 原始字段快照不能代替 getter
- 直接监听子对象与持续读取 prop 入口不是一回事
- 观察到变化与拥有修改权限是两个不同问题
监听用户 ID,getter 保留了重新读取入口
子组件展示某个用户时,如果副作用只依赖 user.id,可以用 getter 每次从当前 props 读取 ID。父层换了用户对象,或者在可追踪的来源中改变 ID,这个入口都能重新检查当前值。直接调用 watch(props.user.id, callback) 则在注册时就把 ID 读取完了,无法凭那个数字或字符串找回来源。
如果 user 可能暂时为空,可用清楚的空值判断,并定义空 ID 时清理旧数据还是等待加载。可选链只是防止访问不存在的对象,不会自动决定业务重置规则。参数变为无效值时仍保留上个用户的数据,可能让页面产生错误归属,应在同步流程中明确处理。
整个对象的内部变化与父层替换分开测试
watch(props.user, callback) 若拿到响应式对象,可以观察当时的这个对象;但父层后来把 user 指向另一个对象,已有监听并不会自动改成另一条属性入口。使用 () => props.user 可以观察替换,需要内部变化时再添加合适的深度,或直接列出真正相关字段。
嵌套对象来自何处也很重要。父层传入普通对象并在响应式系统之外直接修改字段,并不保证子层深监听凭空收到通知。应保持明确的响应式来源或不可变替换协议,避免把所有漏更新都归咎于 watch 语法,再用 deep 无差别扩大观察范围。
解构与只读约定不能忽略编译边界
在 Vue 3.5 的 script setup 中,defineProps 的某些解构访问有编译转换支持,但这不代表普通 JavaScript 解构自动变成引用。向 watch 传递一个已经求值的原始字段仍需要注意来源形式;通过 getter 包装实际读取,可以让接口更清楚,也更容易跨普通函数边界理解。
监听到变化后,子组件通常通过事件或明确动作请求父层更新,不能直接给只读顶层 prop 赋值。嵌套对象可能因引用共享而允许修改,但这会绕过清楚的单向数据流,除非组件契约明确允许共同编辑,否则应由数据所有者负责变更并处理校验与冲突。
容易答错的地方
- 任何 props 属性都绝对不能直接传给 watch
- 若属性本身是有效响应式对象,直接监听可以观察那个对象。真正需要说明的是原始值快照无效,以及直接对象入口未必跟随父层替换,不能把不同情况压成一句绝对规则。
- deep 可以让所有普通嵌套对象都自动响应
- deep 扩大观察范围,不等于把外部任意写入转换成通知。来源本身的响应式或替换协议仍然必须成立。
面试官还会怎么问?
只想在用户 ID 改变时重新请求,需要 deep 吗?
通常不需要,直接监听返回 ID 的 getter 更准确。姓名、头像或其它无关字段变化不应仅因对象很大就触发同一请求。
toRef(props, key) 能用于监听吗?
可以作为属性读取入口,但仍保留 props 的只读约束。需要更复杂的嵌套选择时,getter 往往更直接,也能表达空值处理与派生条件。
父组件每次传同内容的新对象,会触发对象 getter 吗?
对象入口的引用变化可能触发,即使业务字段相同。若副作用只关心稳定 ID,可以监听那个原始字段,避免将包装对象身份误当成业务对象变化。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。