在上文中,我们分析了 Vue 的计算属性实现。本文我们将分析 watch 的实现原理。watch 是 Vue 中另一个重要的响应式特性,它允许我们监听响应式数据的变化并执行相应的回调函数。
一、示例引入
让我们从一个简单的 watch 示例开始:
vue
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 基础用法:监听单个数据源
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
// 监听多个数据源
watch([count, message], ([newCount, newMsg], [oldCount, oldMsg]) => {
console.log(`count: ${oldCount} -> ${newCount}`)
console.log(`message: ${oldMsg} -> ${newMsg}`)
})
// 监听响应式对象
const user = reactive({
name: 'John',
age: 20
})
// deep 选项:深度监听对象的所有属性变化
watch(user, (newValue, oldValue) => {
console.log('user changed:', newValue)
}, { deep: true })
// immediate 选项:立即执行一次回调
watch(count, (newValue) => {
console.log('immediate watch:', newValue)
}, { immediate: true })
</script>
这个例子展示了 watch 的几个重要特性:
- 多种数据源支持:可以监听 ref、reactive 对象、getter 函数等
- 多数据源监听:可以同时监听多个数据源的变化
- 深度监听:通过 deep 选项可以监听对象的深层属性变化
- 立即执行:通过 immediate 选项可以在创建时立即执行一次
- 清理函数:回调函数的第三个参数允许注册清理函数
二、核心实现分析
2.1 watch 函数的入口
watch 函数的实现相对复杂,它需要处理多种不同类型的数据源和选项。让我们看看它的源码实现:
js
export function watch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb?: WatchCallback | null,
options: WatchOptions = EMPTY_OBJ
): WatchHandle {
const {
immediate,
deep,
once,
scheduler,
// 内部选项
augmentJob,
call
} = options
// 处理无效的数据源
const warnInvalidSource = (s: unknown) => {
;(options.onWarn || warn)(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
// ... 后续实现
}
2.2 数据源处理
watch 函数首先需要处理不同类型的数据源,将它们统一转换为 getter 函数:
js
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 1. 处理 ref 类型
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
}
// 2. 处理 reactive 对象
else if (isReactive(source)) {
getter = () => reactiveGetter(source)
forceTrigger = true
}
// 3. 处理数组(多数据源)
else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return reactiveGetter(s)
} else if (isFunction(s)) {
return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
} else {
__DEV__ && warnInvalidSource(s)
}
})
}
// 4. 处理函数
else if (isFunction(source)) {
if (cb) {
// getter with cb
getter = call
? () => call(source, WatchErrorCodes.WATCH_GETTER)
: (source as () => any)
} else {
// watchEffect
getter = () => {
if (cleanup) {
pauseTracking()
try {
cleanup()
} finally {
resetTracking()
}
}
const currentEffect = activeWatcher
activeWatcher = effect
try {
return call
? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
: source(boundCleanup)
} finally {
activeWatcher = currentEffect
}
}
}
}
// 5. 处理无效数据源
else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
2.3 深度监听的实现
当设置了 deep 选项时,watch 会递归遍历被监听的对象:
js
if (cb && deep) {
const baseGetter = getter
const depth = deep === true ? Infinity : deep
getter = () => traverse(baseGetter(), depth)
}
traverse 函数实现了深度遍历:
js
export function traverse(
value: unknown,
depth: number = Infinity,
seen?: Set<unknown>
): unknown {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
depth--
if (isRef(value)) {
traverse(value.value, depth, seen)
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen)
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen)
})
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen)
}
}
return value
}
2.4 副作用创建与调度
watch 内部会创建一个 ReactiveEffect 来追踪依赖变化:
js
const job = (immediateFirstRun?: boolean) => {
if (!(effect.flags & EffectFlags.ACTIVE) || (!effect.dirty && !immediateFirstRun)) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue))
) {
// 执行清理函数
if (cleanup) {
cleanup()
}
const currentWatcher = activeWatcher
activeWatcher = effect
try {
const args = [
newValue,
// 首次执行时传递 undefined 作为旧值
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
boundCleanup
]
call
? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
: cb!(...args)
oldValue = newValue
} finally {
activeWatcher = currentWatcher
}
}
} else {
// watchEffect
effect.run()
}
}
// 创建 effect 实例
effect = new ReactiveEffect(getter)
// 配置调度器
effect.scheduler = scheduler
? () => scheduler(job, false)
: (job as EffectScheduler)
2.5 清理函数的处理
watch 支持在回调函数中注册清理函数,这些清理函数会在下次回调执行前被调用:
js
const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
export function onWatcherCleanup(
cleanupFn: () => void,
failSilently = false,
owner: ReactiveEffect | undefined = activeWatcher
): void {
if (owner) {
let cleanups = cleanupMap.get(owner)
if (!cleanups) cleanupMap.set(owner, (cleanups = []))
cleanups.push(cleanupFn)
} else if (__DEV__ && !failSilently) {
warn(
`onWatcherCleanup() was called when there was no active watcher` +
`to associate with.`
)
}
}
三、总结
通过以上分析,我们详细了解了 Vue watch 的实现原理,主要包括以下几个方面:
-
数据源处理
- 统一转换为 getter 函数
- 支持 ref、reactive、函数等多种类型
- 支持多数据源监听
-
响应式更新
- 基于 ReactiveEffect 实现依赖收集
- 通过调度器控制回调执行时机
- 提供清理函数机制避免副作用残留
-
与计算属性的区别
- 计算属性:用于数据派生,有缓存,惰性执行
- watch:用于副作用处理,无缓存,主动执行
watch 和计算属性虽然都基于 Vue 的响应式系统,但各自有其适用场景:
- 计算属性适合用于数据转换
- watch 适合用于执行副作用(如异步请求、DOM 操作)