Vue3 的 watch 与 watchEffect 的源码级实现原理与区别深度解析
字数 1412 2025-12-08 03:29:18
Vue3 的 watch 与 watchEffect 的源码级实现原理与区别深度解析
1. 核心概念与基本区别
watch 和 watchEffect 都是 Vue3 响应式系统中的副作用函数,但它们的设计目标和用法有本质区别:
watchEffect:立即执行传入的函数,并自动追踪函数内所有响应式依赖。当任意依赖变化时,重新执行该函数。watch:需要明确指定监听的源(单个或多个响应式值),并在源变化时执行回调函数,可以访问变化前后的值。
2. watchEffect 的源码实现原理
2.1 核心函数入口
function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
effect:要执行的副作用函数options:可选的配置对象(如flush、onTrack、onTrigger)
2.2 doWatch 函数的统一处理
function doWatch(source, cb, options) {
// 1. 参数标准化
const { immediate, deep, flush, onTrack, onTrigger } = options || {}
// 2. 创建响应式依赖的 getter 函数
let getter
if (isFunction(source)) {
// watchEffect 的情况:source 本身就是函数
getter = () => {
// 清理上一次的依赖
if (cleanup) {
cleanup()
}
return source(onCleanup)
}
} else {
// watch 的情况:需要处理其他类型(ref、reactive、函数、数组)
// ...(watch 专用逻辑)
}
// 3. 深度监听处理(watchEffect 通常不涉及)
if (deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// 4. 旧值存储(watchEffect 不需要)
let oldValue
if (cb) {
// watch 需要存储旧值
// ...(watch 专用逻辑)
}
// 5. 调度器函数定义
const job = () => {
if (!effect.active) return
if (cb) {
// watch 的执行逻辑
// ...(watch 专用逻辑)
} else {
// watchEffect 的执行逻辑:直接重新运行 effect
effect.run()
}
}
// 6. 创建响应式 effect
const effect = new ReactiveEffect(getter, scheduler)
// 7. 初始执行
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else {
// watchEffect:立即执行一次
effect.run()
}
// 8. 返回停止函数
return () => {
effect.stop()
}
}
2.3 依赖收集的关键机制
当 effect.run() 执行时:
- 会先调用
effect.fn()(即我们传入的副作用函数) - 在函数执行期间,访问任何响应式属性都会触发
track操作 - 这些被访问的属性都会收集当前 effect 作为依赖
- 当这些属性变化时,触发
trigger,重新执行 effect
2.4 清理函数(onCleanup)的实现
function onCleanup(cleanupFn) {
// 将清理函数挂载到 effect 上
effect.onStop = () => {
cleanupFn()
effect.onStop = null
}
}
- 在每次重新执行 effect 前,会先执行上一次的清理函数
- 用于清理副作用(如取消事件监听、清除定时器等)
3. watch 的源码实现原理
3.1 核心函数入口
function watch(source, cb, options) {
return doWatch(source, cb, options)
}
3.2 source 参数的多类型处理
在 doWatch 函数中,针对 watch 的不同用法:
// 处理不同的 source 类型
if (isRef(source)) {
// 监听单个 ref
getter = () => source.value
} else if (isReactive(source)) {
// 监听 reactive 对象
getter = () => source
// 默认启用深度监听
deep = true
} else if (isArray(source)) {
// 监听数组中的多个源
getter = () => source.map(s => {
if (isRef(s)) return s.value
else if (isReactive(s)) return traverse(s)
else if (isFunction(s)) return s()
else return s
})
} else if (isFunction(source)) {
// 监听 getter 函数
getter = source
} else {
// 无效参数
getter = NOOP
}
3.3 新旧值对比与回调触发
// job 函数中的 watch 专用逻辑
const job = () => {
if (!effect.active) return
if (cb) {
// 执行 getter 获取新值
const newValue = effect.run()
// 深度监听或值变化时触发回调
if (deep || hasChanged(newValue, oldValue)) {
// 执行清理函数
if (cleanup) {
cleanup()
}
// 触发回调,传入新值、旧值、清理函数
cb(newValue, oldValue, onCleanup)
// 更新旧值
oldValue = newValue
}
}
}
3.4 immediate 选项的实现
// 初始执行逻辑
if (cb) {
if (immediate) {
// 立即执行回调
job()
} else {
// 只收集依赖,不触发回调
oldValue = effect.run()
}
}
4. 调度器(scheduler)与执行时机控制
4.1 flush 选项的实现
let scheduler
if (flush === 'sync') {
// 同步执行
scheduler = job
} else if (flush === 'post') {
// 组件更新后执行(微任务)
scheduler = () => queuePostFlushCb(job)
} else {
// 默认:pre(组件更新前执行)
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// 组件挂载前立即执行
job()
}
}
}
5. 深度监听(deep)的实现原理
5.1 traverse 递归遍历函数
function traverse(value, seen = new Set()) {
// 1. 基本类型直接返回
if (!isObject(value) || seen.has(value)) {
return value
}
// 2. 避免循环引用
seen.add(value)
// 3. 递归遍历对象属性
if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], seen)
}
} else if (value instanceof Map) {
value.forEach((v, k) => {
traverse(v, seen)
})
} else if (value instanceof Set) {
value.forEach(v => {
traverse(v, seen)
})
} else {
// 普通对象
for (const key in value) {
traverse(value[key], seen)
}
}
return value
}
6. 核心区别总结
6.1 依赖收集方式
watchEffect:自动收集所有在函数执行期间访问的响应式依赖watch:需要显式指定要监听的源,可以是 ref、reactive、getter 函数或它们的数组
6.2 执行时机
watchEffect:立即执行,无法访问旧值watch:默认延迟执行(除非设置immediate: true),可以访问新旧值
6.3 灵活性
watchEffect:更简洁,适合不关心具体哪个依赖变化的场景watch:更精确,可以监听特定值的变化,适合需要旧值的场景
6.4 性能考虑
watchEffect:可能收集到不必要的依赖,导致过度触发watch:依赖明确,触发更精确
7. 实际应用场景
7.1 使用 watchEffect
// 自动追踪所有依赖
const count = ref(0)
const text = ref('')
watchEffect(() => {
console.log(`count: ${count.value}, text: ${text.value}`)
})
// 当 count 或 text 任一变化时都会执行
7.2 使用 watch
// 精确监听特定值
const state = reactive({ count: 0, name: 'Vue' })
watch(
() => state.count, // 只监听 count
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
}
)
// 监听多个源
watch(
[() => state.count, () => state.name],
([newCount, newName], [oldCount, oldName]) => {
// 可以分别处理新旧值
}
)
8. 内存管理与清理
两个 API 都会返回一个停止函数,用于手动停止监听:
const stop = watchEffect(() => {})
// 需要时停止
stop()
9. 性能优化建议
- 对于简单逻辑,优先使用
watchEffect - 需要精确控制依赖时,使用
watch - 使用
onCleanup及时清理副作用 - 避免在
watch中深度监听大型对象(除非必要) - 合理使用
flush选项控制执行时机
这个设计体现了 Vue3 响应式系统的灵活性:watchEffect 提供了便捷的自动依赖追踪,而 watch 提供了更精细的控制能力,两者相辅相成,覆盖了不同的使用场景。