先记住这个答案
watch 可以接收 ref、响应式对象、getter 或这些来源的数组。ref 通常观察它的 value 变化,直接监听 reactive 对象具有默认深层观察行为,getter 则追踪执行时读取的响应式来源,并根据返回值判断变化。watch(state.count, callback) 若传入的是普通数字,就没有提供后续可读取的来源;应使用 getter 或字段 ref。选择监听源应尽量准确表达副作用真正依赖的数据。
- ref、响应式对象与 getter 有不同的默认观察范围
- 普通原始值不是能持续读取变化的监听源
- watch 回调读取其它字段不会自动把它们变成监听源
同一字段可以通过不同入口被观察
例如页面有一个 page ref、一个 filters reactive 对象,以及由两者形成的请求键。监听 page 适合观察页码变化;直接监听 filters 会包含较宽的对象变化范围;监听 getter 返回的请求键,则可以把触发条件压缩为请求真正需要的组合结果。没有一种写法天然适合所有副作用。
如果只关心 filters.status,getter 可以直接返回这个字段,让其它筛选配置变化不必触发相同逻辑。反过来,getter 只返回 filters 对象引用时,不能期待默认行为自动观察它内部所有字段;需要深层观察时应明确表达,或者直接选择对象监听并接受对应范围。
传入原始属性值为什么无法持续观察
调用 watch 时先求值 state.count,得到的是那一刻的数字。数字本身不知道它从哪个对象属性而来,watch 也就无法凭它重新访问源字段。用 () => state.count 延迟读取,或者使用 toRef(state, "count"),才能把来源关系留给响应式系统。
但若 state.child 本身就是一个响应式对象,直接传给 watch 可以观察那个对象的内部变化。这里还要考虑父对象以后是否替换 child:监听旧对象与监听父对象上的 child 入口不同。不能把原始值无效的结论扩展成所有属性都绝对不能直接传。
副作用输入越准确,竞争与成本越容易处理
请求只依赖文档 ID,就不要因为展示主题或草稿光标变化也重新请求。可以让 watch source 返回明确 ID,回调中处理请求取消、错误与迟到结果。回调偶尔读取日志配置并不自动改变监听源,若配置变化本身也需要重跑,就应把它明确纳入来源。
默认回调调度会批量处理同步更新,不能把每次字段赋值都算成一次独立回调。测试时应区分一次状态转换与最终调度结果。需要读取更新后的 DOM 时再考虑对应 flush 时机;监听源决定观察什么,调度选项决定何时执行,两者应分别讨论。
容易答错的地方
- watch 回调里用了哪个字段,就会监听哪个字段
- 这是把 watch 与自动收集回调读取的 watchEffect 混淆。watch 的依赖入口是显式 source,回调读取并不会自动扩展它的观察范围。
- getter 只要被声明,就会随组件每次渲染执行回调
- watch 根据响应式来源变化运行相关判断,不是组件的通用渲染回调。无关组件更新不能自动代替 source 的变化通知。
面试官还会怎么问?
可以一次监听多个来源吗?
可以传来源数组,例如一个 ref 加一个字段 getter,回调的新旧值也按位置组成数组。数组里的原始字段快照仍不能冒充有效的动态来源。
ref 保存对象时,内部字段变化默认会触发吗?
普通监听该 ref 通常关注 value 的变化,内部字段修改需要深层选项或更具体的字段 getter。对象本身具有响应式能力,不等于所有监听入口默认都深度遍历。
只是计算展示结果也应该 watch 吗?
通常先考虑 computed 或直接纯计算。watch 适合变化后需要执行外部同步或其它明确副作用的场景,额外保存可派生值会增加同步成本。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。