5、Vue3 watch与watchEffect深度解析
·
目录
一、watch 的工作原理
1. 基本概念
watch 是一个显式的侦听器,需要明确指定要监听的数据源。
2. 工作流程
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Tom', age: 18 })
// 1. 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count从 ${oldVal} 变为 ${newVal}`)
})
// 2. 监听 reactive 对象的某个属性(需要用 getter 函数)
watch(
() => state.name,
(newVal, oldVal) => {
console.log(`name从 ${oldVal} 变为 ${newVal}`)
}
)
// 3. 监听多个数据源
watch(
[count, () => state.name],
([newCount, newName], [oldCount, oldName]) => {
console.log('多个值发生变化')
}
)
// 4. 深度监听
watch(
() => state,
(newVal) => {
console.log('state 深层变化')
},
{ deep: true }
)
3. 核心原理图解
- 显式依赖: 你必须明确告诉 Vue 你要观察哪一个变量(Ref、Reactive、Getter 函数或其数组)。
- 懒执行(默认): 默认情况下,
watch是惰性的。只有当被监听的源发生变化时,才会执行回调。 - 获取前后值: 回调函数会收到两个参数:
newValue(新值)和oldValue(旧值)。 - 内部实现: Vue 内部会将监听源封装成一个 Effect。当监听源发生变化时,调度器会触发回调函数的执行。
┌─────────────────────────────────────────────────────┐
│ watch 原理 │
├─────────────────────────────────────────────────────┤
│ │
│ 指定数据源 ──────► 依赖收集 ──────► 变化时触发回调 │
│ (source) (track) (trigger) │
│ │
│ 特点: │
│ • 惰性执行(默认不立即执行) │
│ • 可以获取新旧值 │
│ • 明确知道是哪个依赖变化 │
│ │
└─────────────────────────────────────────────────────┘
4. 源码简化版
function watch(source, cb, options = {}) {
let getter
// 1. 标准化 source 为 getter 函数
if (isRef(source)) {
getter = () => source.value
} else if (isReactive(source)) {
getter = () => source
options.deep = true // reactive 对象默认深度监听
} else if (isFunction(source)) {
getter = source
}
let oldValue
// 2. 创建调度任务
const job = () => {
const newValue = effect.run()
cb(newValue, oldValue)
oldValue = newValue
}
// 3. 创建响应式 effect
const effect = new ReactiveEffect(getter, job)
// 4. 根据配置决定是否立即执行
if (options.immediate) {
job()
} else {
oldValue = effect.run()
}
}
二、watchEffect 的工作原理
1. 基本概念
watchEffect 是一个隐式的侦听器,会自动追踪回调函数中使用的所有响应式依赖。
2. 工作流程
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Tom' })
// 1. 基本用法 - 自动追踪依赖
watchEffect(() => {
// 自动追踪 count.value 和 state.name
console.log(`count: ${count.value}, name: ${state.name}`)
})
// 2. 清除副作用
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log(count.value)
}, 1000)
// 在下次执行前或组件卸载时调用
onCleanup(() => {
clearInterval(timer)
})
})
// 3. 停止监听
const stop = watchEffect(() => {
console.log(count.value)
})
// 手动停止
stop()
3. 核心原理图解
- 自动依赖收集: 不需要显式指定监听谁。它会立即执行一次,并在此过程中记录下函数内访问过的所有响应式属性。
- 立即执行: 它在组件初始化时会立即运行一次(相当于
watch设置了immediate: true)。 - 不追踪旧值: 它只关心当前的逻辑,不会记录或提供数据变化前的值。
- 内部实现: 它将传入的整个函数视为一个 Effect。执行时,任何触发的
getter都会将其添加为依赖,只要依赖变了,整个函数重新运行。
┌─────────────────────────────────────────────────────┐
│ watchEffect 原理 │
├─────────────────────────────────────────────────────┤
│ │
│ 执行回调 ──────► 自动收集依赖 ──────► 依赖变化重新执行│
│ (run) (track) (trigger) │
│ │
│ 特点: │
│ • 立即执行(非惰性) │
│ • 无法获取旧值 │
│ • 自动追踪所有依赖 │
│ │
└─────────────────────────────────────────────────────┘
4. 源码简化版
function watchEffect(effect, options = {}) {
// 本质上是 watch 的简化版
return doWatch(effect, null, options)
}
function doWatch(source, cb, options) {
const getter = isFunction(source) ? source : () => traverse(source)
let cleanup
const onCleanup = (fn) => {
cleanup = fn
}
const job = () => {
// 执行清理函数
if (cleanup) {
cleanup()
}
// 执行 effect,传入 onCleanup
effect.run()
}
const effect = new ReactiveEffect(
() => getter(onCleanup),
job
)
// watchEffect 默认立即执行
effect.run()
}
三、具体区别
1. 对比表格
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式指定 | 自动追踪 |
| 立即执行 | 默认否(可配置 immediate) | 默认是 |
| 获取旧值 | ✅ 可以 | ❌ 不可以 |
| 深度监听 | 需配置 deep: true | 自动(访问到就追踪) |
| 精确控制 | 更精确 | 较宽松 |
| 适用场景 | 特定数据变化执行逻辑 | 副作用同步 |
2. 代码对比
const userId = ref(1)
const userData = ref(null)
// ============ watch 写法 ============
watch(userId, async (newId, oldId) => {
console.log(`用户ID从 ${oldId} 变为 ${newId}`)
userData.value = await fetchUser(newId)
}, { immediate: true })
// ============ watchEffect 写法 ============
watchEffect(async () => {
// 自动追踪 userId.value
userData.value = await fetchUser(userId.value)
})
3. 执行时机对比
const count = ref(0)
// watch - 默认不立即执行
watch(count, (val) => {
console.log('watch:', val) // 初始不打印
})
// watchEffect - 立即执行
watchEffect(() => {
console.log('watchEffect:', count.value) // 立即打印: 0
})
count.value++
// 输出:
// watchEffect: 0 (立即执行)
// watch: 1
// watchEffect: 1
4. 使用场景建议
// ✅ 使用 watch 的场景
// 1. 需要获取变化前后的值
watch(searchText, (newVal, oldVal) => {
analytics.track('search_changed', { from: oldVal, to: newVal })
})
// 2. 需要条件性地执行副作用
watch(status, (newStatus) => {
if (newStatus === 'error') {
showErrorModal()
}
})
// 3. 监听特定属性的变化
watch(() => props.id, fetchData)
// ✅ 使用 watchEffect 的场景
// 1. 多个依赖的副作用同步
watchEffect(() => {
document.title = `${user.name} - ${page.title}`
})
// 2. 需要立即执行的副作用
watchEffect(() => {
localStorage.setItem('theme', theme.value)
})
// 3. 依赖关系复杂或动态变化
watchEffect(() => {
if (showA.value) {
console.log(dataA.value)
} else {
console.log(dataB.value)
}
})
四、面试怎么回答才精彩
🌟 精彩回答模板
面试官:请说说 watch 和 watchEffect 的区别?
第一层:核心区别(30秒)
"watch 和 watchEffect 最核心的区别在于依赖追踪方式:
watch是显式追踪,需要明确指定监听的数据源watchEffect是隐式追踪,自动收集回调中使用的所有响应式依赖"
第二层:具体差异(1分钟)
"具体来说有四点不同:
- 执行时机:watch 默认惰性执行,watchEffect 立即执行
- 新旧值:watch 可以获取变化前后的值,watchEffect 不行
- 精确度:watch 更精确,watchEffect 更便捷
- 使用场景:watch 适合监听特定数据做逻辑处理,watchEffect 适合副作用同步"
第三层:原理层面(加分项)
"从原理上看,它们底层都是基于 ReactiveEffect 实现的:
// 简化理解
watchEffect(fn) ≈ watch(fn, null, { immediate: true })
watch 会将数据源标准化为 getter 函数,在 getter 执行时收集依赖;而 watchEffect 直接执行回调函数,在执行过程中自动追踪所有访问到的响应式数据。"
第四层:实战经验(亮点)
"在实际项目中,我的选择原则是:
- 需要对比新旧值 → 用 watch
- 需要条件判断是否执行 → 用 watch
- 多依赖的副作用同步 → 用 watchEffect
- 依赖关系动态变化 → 用 watchEffect
比如做搜索功能时,我会用 watch 监听搜索词,因为可能需要防抖和对比变化;而做主题切换同步到 localStorage,我会用 watchEffect,因为更简洁。"
🎯 面试加分话术
"其实 Vue3 还提供了 watchPostEffect 和 watchSyncEffect,
它们是 watchEffect 的别名,区别在于 flush 时机:
- watchEffect = watchEffect + { flush: 'pre' }
- watchPostEffect = watchEffect + { flush: 'post' } // DOM 更新后执行
- watchSyncEffect = watchEffect + { flush: 'sync' } // 同步执行
这在需要访问更新后的 DOM 时很有用。"
📝 一句话总结
watch 是"我告诉你监听谁",watchEffect 是"你自己看着办"。前者精确可控,后者简洁自动。
更多推荐


所有评论(0)