Vue3 watch 如何建立响应式联系
Vue 3 的 watch API 建立响应式联系的核心逻辑位于 packages/runtime-core/src/apiWatch.ts 中。
1. watch 的第一个参数是做什么的?
watch 的第一个参数 source 用于“收集依赖”,即追踪你想要监听的数据变化。它可以是响应式数据(如 reactive/ ref)、getter 函数,也可以是多个响应式数据组成的数组。
2. 源码关键片段分析
源码部分如下:
TypeScript
...
export type WatchSource<T = any> = Ref<T, any> | ComputedRef<T> | (() => T) //这里限制watchsource的类型为 ref 计算属性 ()=>T getter方法
...
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>, // source的类型为传递的soruce本身或者为watchSource所规定的类型
cb: any,
options?: WatchOptions<Immediate>,
): WatchHandle {
...
return doWatch(source as any, cb, options)
}
在 doWatch 里,source 会直接传递给 baseWatch:
TypeScript
const watchHandle = baseWatch(source, cb, baseWatchOptions)
而 baseWatch(在 @vue/reactivity 包里)会根据 source 的类型来决定如何追踪依赖:
如果
source是响应式数据(ref/ reactive)
Vue 内部已经对这些数据做了 Proxy 代理,能自动追踪依赖和收集变化。如果
source不是响应式数据
比如直接传一个普通变量或字面量,那么它的变化 Vue 无法感知,这样 watch 就永远不会被触发!
3. 为什么不是响应式数据要用 getter?
getter 本质上是一个函数,Vue 在 watch 时会执行这个函数,并在执行过程中收集依赖。
只有 getter 里读取了响应式数据,Vue 才能追踪它们的变化。
源码体现
比如实例方法 instanceWatch:
TypeScript
const getter = isString(source)
? source.includes('.')
? createPathGetter(publicThis, source)
: () => publicThis[source]
: source.bind(publicThis, publicThis)
...
const res = doWatch(getter, cb.bind(publicThis), options)
这里会把普通的属性名、路径字符串转成一个“getter 函数”,保证 getter 内部访问的是响应式数据。
4. 总结
watch 的第一个参数必须是响应式数据,否则变化无法被追踪。
如果不是响应式数据(如普通变量/常量),要用 getter 函数,且 getter 必须访问响应式数据。
getter 的存在是为了让 Vue 能够在 getter 执行时收集依赖,实现自动追踪。
举例
js
const count = ref(0)
// 推荐:直接传响应式数据
watch(count, (newVal, oldVal) => { ... })
// 推荐:getter 里读取响应式数据
watch(() => someReactiveObj.value, (newV, oldV) => { ... })
// 错误用法:传普通变量
let foo = 1
watch(foo, ...) // foo 不是响应式,watch 无法监听
结论:
watch 的第一个参数要么是响应式数据,要么是 getter(getter 内部要访问响应式数据),这样 Vue 才能精准跟踪依赖,触发回调。