一、前言
在上文中,我们分析了 Vue3 的 watch API 实现。本文我们将分析另一个重要的响应式 API —— watchEffect。与 watch 不同,watchEffect 不需要指定监听的数据源,它会自动追踪回调函数中使用的所有响应式依赖。
二、示例引入
让我们从一个简单的例子开始:
<script setup>
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 自动追踪 count 和 message 的变化
watchEffect(() => {
console.log(`Count: ${count.value}, Message: ${message.value}`)
})
// 支持清理副作用
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log(count.value)
}, 1000)
// 在下次执行或停止时清理
onCleanup(() => clearInterval(timer))
})
// 控制执行时机
watchEffect(() => {
console.log('DOM updated:', count.value)
}, { flush: 'post' })
</script>
这个例子展示了 watchEffect 的几个重要特性:
- 自动依赖追踪:不需要显式指定依赖
- 清理机制:支持在重新执行前清理副作用
- 执行时机控制:可以控制副作用函数的执行时机
三、核心实现分析
3.1 watchEffect 的入口
watchEffect 的实现相对简单,它是对 doWatch 函数的封装:
export function watchEffect(
effect: WatchEffect,
options?: WatchEffectOptions,
): WatchHandle {
return doWatch(effect, null, options)
}
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions,
): WatchHandle {
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions,
): WatchHandle {
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
)
}
3.2 doWatch 的实现
doWatch 函数是 watch 和 watchEffect 的统一实现。与 watch 不同,watchEffect 在调用时传入的 cb 参数为 null,这使得它走向了不同的实现路径:
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null, // watchEffect 传入 null
options: WatchOptions = EMPTY_OBJ,
): WatchHandle {
// 1. 处理选项
const { immediate, deep, flush, once } = options
// 2. 开发环境警告
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`,
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`,
)
}
}
// 3. 配置调度器
let isPre = false
if (flush === 'post') {
baseWatchOptions.scheduler = job => {
queuePostRenderEffect(job, instance && instance.suspense)
}
} else if (flush !== 'sync') {
// default: 'pre'
isPre = true
baseWatchOptions.scheduler = (job, isFirstRun) => {
if (isFirstRun) {
job()
} else {
queueJob(job)
}
}
}
// 1. 处理 source 参数
let getter: () => any
if (!cb) {
// watchEffect 的情况:source 本身就是 effect 函数
getter = () => {
// 执行 source 函数时会自动收集依赖
if (cleanup) {
cleanup()
}
return source(onCleanup)
}
} else {
// watch 的情况:需要手动指定监听源
getter = () => traverse(source)
}
// 2. 创建 effect 实例
const effect = new ReactiveEffect(getter)
// 5. 配置调度器
effect.scheduler = scheduler
? () => scheduler(job, false)
: (job as EffectScheduler)
// 6. 首次执行,收集依赖
if (cb) {
if (immediate) {
job(true)
} else {
oldValue = effect.run()
}
} else {
// watchEffect 的情况:直接执行 effect
effect.run()
}
// 7. 返回停止函数
return () => {
effect.stop()
cleanup && cleanup()
}
}
3.2.1 自动依赖收集原理
watchEffect 的自动依赖收集主要通过以下机制实现:
- 直接使用回调函数作为 getter
// watchEffect 的情况
getter = () => {
if (cleanup) {
cleanup()
}
return source(onCleanup)
}
- 不需要像 watch 那样手动指定数据源
- 回调函数中访问的响应式数据会被自动收集为依赖
- 依赖收集过程
// ReactiveEffect 的 run 方法
run() {
// 设置当前活跃的 effect
activeEffect = this
try {
// 执行 getter 函数,此时会访问响应式数据
// 响应式数据的 get 操作会自动收集当前的 activeEffect
return this.fn()
} finally {
activeEffect = undefined
}
}
- 响应式数据的依赖收集
// ref 的实现
class RefImpl {
get value() {
track(this, TrackOpTypes.GET, 'value')
return this._value
}
}
// reactive 的实现
function createGetter() {
return function get(target, key) {
const res = Reflect.get(target, key)
track(target, TrackOpTypes.GET, key)
return res
}
}
这种设计带来的好处是:
- 使用更简单:不需要显式声明依赖项
- 依赖自动管理:依赖会随着回调函数的执行自动收集和清理
- 更少的心智负担:不用担心遗漏某个依赖项
举例说明:
const count = ref(0)
const message = ref('Hello')
// 不需要显式指定监听 count 和 message
watchEffect(() => {
// 访问 count.value 和 message.value 时会自动收集依赖
console.log(`${count.value}: ${message.value}`)
})
// 相比之下,watch 需要显式指定依赖源
watch([count, message], ([newCount, newMessage]) => {
console.log(`${newCount}: ${newMessage}`)
})
3.2.2 watchEffect 的本质设计
watchEffect 的一个重要设计思想是: 它本质上就是把传入的 source 函数同时作为依赖收集的 getter 和响应变化的回调函数。这种设计带来了几个优点:
- 简化心智模型
// watch 需要分别指定数据源和回调
watch(source, (newValue) => {
// 处理变化
})
// watchEffect 统一在一个函数中完成
watchEffect(() => {
// 访问数据的同时也处理变化
})
-
统一职责
- 不需要分离"要监听什么"和"数据变化后要做什么"
- 在同一个上下文中处理相关的逻辑,提高了代码的内聚性
- 减少了可能的逻辑分散和依赖管理的复杂性
-
更符合直觉
const count = ref(0)
const message = ref('Hello')
// 直接描述"我要做什么",而不是"我要监听什么,然后做什么"
watchEffect(() => {
console.log(`当前状态: ${count.value}, ${message.value}`)
})
这种设计反映了 Vue3 的一个重要理念: 通过统一和简化 API 的设计,来降低开发者的使用成本。watchEffect 通过将 source 函数同时作为 getter 和 callback,巧妙地实现了这一目标。
3.3 执行时机控制
watchEffect 支持三种执行时机:
- pre(默认) :组件更新前执行
// 默认 pre 模式
baseWatchOptions.scheduler = (job, isFirstRun) => {
if (isFirstRun) {
job()
} else {
queueJob(job)
}
}
- post:组件更新后执行
// post 模式
baseWatchOptions.scheduler = job => {
queuePostRenderEffect(job, instance && instance.suspense)
}
- sync:同步执行
// sync 模式不使用调度器,直接执行
if (flush === 'sync') {
// 直接执行,不进入队列
effect.run()
}
3.4 清理机制
watchEffect 提供了清理机制,用于清理副作用:
watchEffect((onCleanup) => {
// 注册清理函数
onCleanup(() => {
// 清理逻辑
})
})
清理函数的实现:
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)
}
}
3.5 停止监听
watchEffect 返回一个停止函数,用于停止监听:
const stop = watchEffect(() => {
/* ... */
})
// 停止监听
stop()
停止函数的实现:
const watchHandle = () => {
effect.stop()
cleanup && cleanup()
}
watchHandle.pause = effect.pause.bind(effect)
watchHandle.resume = effect.resume.bind(effect)
watchHandle.stop = watchHandle
return watchHandle
四、总结
通过以上分析,我们了解了 watchEffect 的核心实现:
-
简化的 API
- 不需要显式指定依赖
- 自动追踪响应式依赖
- 更简洁的使用方式
-
灵活的执行控制
- 支持三种执行时机
- 可以控制副作用的执行顺序
- 提供暂停和恢复功能
-
完善的清理机制
- 支持副作用清理
- 自动在重新执行前调用清理函数
- 停止监听时也会执行清理
与 watch 相比,watchEffect 的特点是:
-
watch:
- 需要明确指定监听的数据源
- 可以访问新值和旧值
- 支持深度监听选项
-
watchEffect:
- 自动收集依赖
- 立即执行
- 更适合处理副作用
在实际开发中,我们应该根据具体场景选择合适的 API:
- 需要比较新旧值时,使用 watch
- 需要执行副作用时,使用 watchEffect
- 需要控制执行时机时,使用对应的 watchEffect 变体