先记住这个答案
watch 把监听源与副作用回调分开,只跟踪显式源中的响应式读取,回调内额外读到的状态不会自动加入源。watchEffect 在效果函数同步执行时自动收集实际读取的响应式依赖,默认创建后就执行一次。需要精确限定触发条件或比较新旧值时选 watch;副作用自然依赖多个读取值且不需要旧值时,watchEffect 可以减少重复声明。
- watch 的依赖来自显式源
- watchEffect 跟踪同步阶段实际读取
- 接口选择取决于触发边界和旧值需求
额外读取不等于额外触发
例如资源编号变化时请求详情,回调顺便读取当前语言作为日志信息。如果用 watch 只监听编号,语言变化不会重新触发;若用 watchEffect 同步读取编号和语言,两者都可能让效果重跑。
这不是谁更完整,而是产品语义不同。语言如果确实决定服务端返回内容,就应该成为请求源的一部分;若只是日志标签,则不应因为用户切换语言而无意发起新的数据请求。
在同一组状态上对照行为
示例让两个观察者同时读取编号和语言,但 watch 只显式监听编号。改变语言后等待刷新,可以看到 effect 的记录增加,而 watch 的记录不变,说明回调读取并没有扩展其源。
默认 watch 不立即执行,示例加 immediate 只是为了让两边都留下初值记录,从而便于比较后续变化。这个配置不改变显式依赖与自动依赖的根本区别。
import { ref, watch, watchEffect } from 'vue'
const id = ref('A')
const locale = ref('zh')
const explicit: string[] = []
const automatic: string[] = []
const stopWatch = watch(id, (value) => {
explicit.push(value + ':' + locale.value)
}, { immediate: true })
const stopEffect = watchEffect(() => {
automatic.push(id.value + ':' + locale.value)
})把 locale 改为 en 并等待 nextTick 后,explicit 仍只有 A:zh,automatic 多出 A:en。停止两个句柄后再改变编号均不应追加记录,实验结束时应释放这两份观察。
自动依赖仍需要可读的函数边界
当效果函数越来越长,或者调用许多会读取响应式状态的辅助函数时,触发范围可能不再直观。把纯计算拆为 computed、把请求条件整理为显式多源,有助于让未来维护者理解什么会重启副作用。
条件分支也会影响当前收集范围。开关关闭时提前返回,后面未读取的字段不会成为这次的依赖;开关打开后执行到新分支,才会收集相应读取,因此调试时要结合实际运行路径。
容易答错的地方
- 认为 watchEffect 会深度监听所有对象
- 自动收集的是执行过程中触及的响应式操作,不是自动对所有引用做完整递归遍历。若只读取一个对象引用而没有读取内部属性,不能据此假定每个嵌套字段变化都会触发。
- 只因为代码更短就选择自动依赖
- 依赖范围是行为契约的一部分。效果里新增一行响应式读取可能意外增加触发条件;如果请求成本高或业务要求严格控制重跑条件,显式源往往更容易在代码审查中确认。
面试官还会怎么问?
watch 回调里读 computed 会增加监听源吗?
不会自动把该读取加入这个 watch 的源依赖。若希望 computed 变化能够触发,应把它作为源或放进源 getter 中,不能仅在回调里使用它的当前值就认为已经订阅。
两种接口都能执行异步请求吗?
都可以,但都需要处理过期结果和清理。watchEffect 还要把决定请求的响应式读取放在同步阶段;选择接口并不会让请求按发起顺序完成,也不会自动解决旧响应覆盖问题。
业务需要知道变化前的编号时用什么?
通常选择 watch,它明确提供新旧值并允许多源数组。watchEffect 的参数是清理注册函数,不是旧值;若强行自己缓存历史,还要额外定义首次运行和对象快照的语义。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。