Appearance
watch 与 watchEffect:侦听器的实现原理与差异对比
reactive、ref 和 computed 提供了状态和派生状态。watch 和 watchEffect 则提供了 ** “状态变化时执行副作用” **的能力(例如 API 请求、DOM 操作)。
本节将分析这两种侦听器的实现原理和核心差异。
侦听器的“双层架构”
Vue 的侦听器设计分为两层:
- 底层 (
@vue/reactivity):提供baseWatch函数,只关心数据订阅与调度,不关心组件。 - 上层 (
@vue/runtime-core):封装watch和watchEffectAPI,增加了与组件生命周期绑定(自动停止)、flush调度(pre/post/sync)等功能。
我们重点分析上层的 doWatch,它是 watch 和 watchEffect 的共同实现入口。
watch:精确的侦听器
watch 的核心是:侦听“指定”的数据源,并在其变化时执行“回调函数”。
javascript
// 侦听一个 ref
watch(count, (newValue, oldValue) => {
// ...
})
// 侦听一个 getter 函数
watch(
() => state.user.name,
(newName, oldName) => {
// ...
}
)watch 的实现主要分为三步:创建 getter、封装 effect、定义 job (回调)。
创建 getter:定义“侦听什么”
doWatch 首先会根据传入的 source(数据源),将其统一转换为一个 getter 函数。这个 getter 的职责就是“读取数据”。
typescript
// doWatch 内部的 getter 创建逻辑 (简化)
let getter: () => any
if (isRef(source)) {
// 1. 如果源是 ref
// getter 的任务就是读取 .value
getter = () => source.value
} else if (isReactive(source)) {
// 2. 如果源是 reactive 对象
// getter 的任务是深度遍历它(默认 deep: true)
getter = () => traverse(source)
} else if (isArray(source)) {
// 3. 如果源是数组
// getter 的任务是遍历数组,分别读取每一项
getter = () => source.map(s => {
if (isRef(s)) return s.value
if (isReactive(s)) return traverse(s)
if (isFunction(s)) return s()
})
} else if (isFunction(source)) {
// 4. 如果源已经是 getter 函数
// 直接使用
getter = source
}deep 选项的实现 (traverse): 如果用户设置了 deep: true(对 reactive 对象默认开启),getter 会被再次包装:
typescript
if (cb && deep) {
const baseGetter = getter
// 深度遍历,访问对象/数组的每一个属性
// 目的是在 effect 运行时,“触碰”到所有嵌套属性
// 从而收集到所有嵌套属性的依赖
getter = () => traverse(baseGetter())
}traverse 函数会递归访问一个对象的所有属性,强制 track(收集依赖)它们。
封装 effect:建立响应式连接
有了 getter,Vue 就会用它创建一个 ReactiveEffect。
typescript
const effect = new ReactiveEffect(
// 1. effect 要执行的任务:运行 getter()
getter,
// 2. effect 的调度器 (scheduler):
// 当 getter 依赖的数据变化时,不要立即重新 run()
// 而是执行这个 scheduler,即我们定义的“回调任务” (job)
() => job()
)这体现了 watch 和 computed 的核心区别:
computed的scheduler只是为了设置_dirty = true(标记为脏)。watch的scheduler是为了执行用户定义的回调函数(job)。
定义 job (回调):“变化后做什么”
job(任务)是 scheduler 调用的核心,它封装了 watch 回调(cb)的完整执行逻辑。
typescript
// watch 内部的回调任务 (job) (简化)
let oldValue = //... 初始值
let newValue = //...
const job = () => {
if (!effect.active) {
return // 侦听器已停止
}
if (cb) { // 必须有回调函数 (watchEffect 没有 cb)
// 1. 【重新求值】
// 再次运行 getter(),获取“新值”
// 同时也会重建依赖
newValue = effect.run()
// 2. 【对比】
// 对比新旧值 (deep 选项会影响对比)
if (
deep ||
hasChanged(newValue, oldValue)
) {
// 3. 【清理】
// 如果注册了 onCleanup,先执行上一次的清理函数
if (cleanup) {
cleanup()
}
// 4. 【执行回调】
// 调用用户传入的 cb(newValue, oldValue, onCleanup)
cb(newValue, oldValue, onCleanup)
// 5. 【更新旧值】
oldValue = newValue
}
}
}选项的实现:immediate 与 flush
immediate: true默认情况下,watch只在数据变化后才执行回调。 如果设置了immediate: true,watch会在初始化时就立即执行一次job,此时oldValue为undefined。flush: 'pre' | 'post' | 'sync'flush选项控制scheduler调用job的时机:sync:trigger发生时,job()同步执行。(谨慎使用)pre(默认):scheduler使用queueJob(job),将job放入组件更新前的队列中。post:scheduler使用queuePostRenderEffect(job),将job放入组件更新后的队列中。(适合需要操作 DOM 的回调)
watchEffect:自动收集依赖的侦听器
watchEffect 的实现是 watch 的一种特例。
javascript
// watchEffect 是 doWatch 的一种调用方式
export function watchEffect(
effect: WatchEffect, // 即 source
options?: WatchEffectOptions,
): WatchHandle {
// 它调用 doWatch,但:
// 1. source 就是那个要“立即执行”的函数
// 2. cb (回调) 为 null
// 3. options 中,immediate 默认为 true
return doWatch(effect, null, options)
}watchEffect 的内部逻辑如下:
getter:就是你传入的那个函数(effect)。effect:用这个getter封装。job(回调): 由于cb为null,job的逻辑简化为:javascript它没有const job = () => { // 1. 【清理】 // 执行 onCleanup if (cleanup) { cleanup() } // 2. 【重新执行】 // 再次运行 getter(),即用户传入的函数 // 在运行时,它会自动收集“新”的依赖 newValue = effect.run() }cb,getter的执行本身就是“副作用”。
onCleanup 的实现watchEffect (或 watch 的 cb) 在执行时,会设置一个全局的 activeWatcher。onCleanup 函数会把清理回调注册到这个 activeWatcher 上。当 job 下次执行时,会先检查并调用已注册的 cleanup 函数。
watch vs watchEffect:核心差异对比
| 特性 | watch | watchEffect |
|---|---|---|
| 数据源 | 明确指定 (source)。 | 自动收集 (函数体内用到的所有依赖)。 |
| 回调 | cb 函数。数据源和副作用分离。 | 没有 cb。副作用就是 source 函数本身。 |
| 立即执行 | 默认惰性(lazy: true)。需 immediate: true 开启。 | 总是立即执行(immediate: true)。 |
| 旧值访问 | 支持(cb(newValue, oldValue))。 | 不支持。 |
deep | reactive 对象默认 deep: true。getter 函数默认 deep: false。 | 总是深度侦听(traverse)。 |
使用场景对比:
使用
watch当你...- 需要访问旧值:
watch(count, (newVal, oldVal) => ...)。 - 需要惰性执行:只想在数据变化后才执行,不想立即执行。
- 想精确控制:只想侦听
state.user.name,而不想侦听state.user.age。
- 需要访问旧值:
使用
watchEffect当你...- 想自动收集依赖:你不在乎依赖了多少数据,只要函数体内的任何依赖变了,就重新执行。
- 代码更简洁:比如
watchEffect(() => localStorage.setItem('key', state.data))。 - 只需要“副作用”,不需要“新/旧值”对比。
总结
watch 和 watchEffect 都是基于 ReactiveEffect 实现的响应式侦听器。
watch- 分离了“数据源 (
getter)”和“副作用 (cb)”。 - 提供
oldValue、immediate: false(惰性)和deep等精确控制。
- 分离了“数据源 (
watchEffect- “数据源”和“副作用”合二为一。
- 立即执行并自动收集函数体内的所有依赖。
理解它们都是“创建 getter -> 封装 effect -> 定义 scheduler (即 job)”的产物,能帮助我们根据实际场景选择最合适的 API。
