watch与watchEffect的区别与实现
watch与watchEffect的基本概念
watch和watchEffect都是Vue3中用于响应式数据监听的API,它们基于相同的响应式系统构建,但在使用方式和内部实现上有显著差异。watch需要显式指定要监听的数据源和回调函数,而watchEffect会自动追踪其内部访问的响应式依赖。
// watch示例
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// watchEffect示例
const state = reactive({ price: 100, quantity: 2 })
watchEffect(() => {
console.log(`Total: ${state.price * state.quantity}`)
})
依赖收集机制差异
watch的依赖收集发生在初始化阶段,需要明确指定监听目标。Vue会将这些目标转换为getter函数,在组件渲染时建立依赖关系。而watchEffect采用即时依赖收集模式,在执行副作用函数时动态建立依赖。
// watch的依赖是静态指定的
watch(
[() => obj.a, () => obj.b],
([a, b], [prevA, prevB]) => {
/* ... */
}
)
// watchEffect的依赖是动态收集的
watchEffect(() => {
// 只有实际访问的属性会被追踪
if (condition.value) {
console.log(obj.a)
} else {
console.log(obj.b)
}
})
执行时机与调度控制
watch默认是懒执行的,只有依赖变化时才会触发回调,且默认情况下回调的执行是异步的。watchEffect则会立即执行一次,后续依赖变化时也会异步执行。两者都支持通过options.flush控制调度时机。
// watch的延迟执行特性
const data = ref(null)
watch(data, (newVal) => {
// 不会立即执行,只有data变化时触发
})
// watchEffect的立即执行特性
watchEffect(() => {
// 立即执行一次,后续data变化时再执行
console.log(data.value)
})
// 调度控制示例
watchEffect(
() => { /* ... */ },
{
flush: 'post', // 在组件更新后执行
onTrack(e) { debugger }, // 调试钩子
onTrigger(e) { debugger }
}
)
源码实现对比
在Vue源码中,watch和watchEffect都通过doWatch
函数实现核心逻辑,但参数处理不同。watch需要先标准化source为getter函数,而watchEffect直接使用传入的函数。
// runtime-core/src/apiWatch.ts 简化版实现
function watch(source, cb, options) {
return doWatch(source, cb, options)
}
function watchEffect(effect, options) {
return doWatch(effect, null, options)
}
function doWatch(
source: WatchSource | WatchEffect,
cb: WatchCallback | null,
options: WatchOptions
) {
// 标准化source为getter函数
let getter: () => any
if (isFunction(source)) {
getter = () => source()
} else {
getter = () => traverse(source)
}
// 清理函数处理
let cleanup: () => void
const onCleanup = (fn: () => void) => {
cleanup = runner.onStop = () => fn()
}
// 调度器实现
const scheduler = () => {
if (!runner.active) return
if (cb) {
// watch的处理逻辑
const newValue = runner()
if (hasChanged(newValue, oldValue)) {
callWithAsyncErrorHandling(cb, [
newValue,
oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect的处理逻辑
runner()
}
}
// 创建响应式effect
const runner = effect(getter, {
lazy: true,
scheduler,
onTrack: options.onTrack,
onTrigger: options.onTrigger
})
// 初始执行逻辑
if (cb) {
oldValue = runner()
} else {
runner()
}
}
停止监听与副作用清理
两者都返回一个停止函数,调用它可以取消监听。watchEffect更常用于需要自动清理副作用的场景,通过onCleanup注册清理函数。
// 停止监听示例
const stop = watchEffect(() => { /* ... */ })
stop() // 取消监听
// 副作用清理示例
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('Running')
}, 1000)
onCleanup(() => clearInterval(timer))
})
性能优化与使用场景
watch适合精确监听特定数据的变化,特别是需要访问旧值的场景。watchEffect适合依赖关系复杂或需要自动追踪所有访问的响应式属性的情况。
// watch适合的场景
watch(
() => route.params.id,
(newId, oldId) => {
fetchData(newId)
}
)
// watchEffect适合的场景
watchEffect(() => {
// 自动追踪所有使用的响应式属性
document.title = `${user.name} - ${page.title}`
})
深度监听与立即执行
watch支持深度监听对象和数组的变化,通过deep: true
配置。watchEffect由于自动追踪所有访问的属性,不需要单独配置深度监听。
// watch的深度监听
const obj = reactive({ nested: { count: 0 } })
watch(
() => obj,
(newVal) => {
console.log('nested changed', newVal.nested.count)
},
{ deep: true }
)
// watchEffect自动深度追踪
watchEffect(() => {
// 任何层级的访问都会被追踪
console.log(obj.nested.count)
})
调试能力比较
两者都支持onTrack和onTrigger调试钩子,但watchEffect由于自动收集依赖的特性,调试时可能更复杂。watch的明确依赖声明使得调试相对直观。
watchEffect(
() => { /* effect */ },
{
onTrack(e) {
debugger // 依赖被追踪时触发
},
onTrigger(e) {
debugger // 依赖变化触发effect时触发
}
}
)
响应式API的协同工作
watch和watchEffect与Vue3的其他响应式API如ref、reactive、computed等协同工作时表现一致,但watchEffect对计算属性的处理有细微差别。
const count = ref(0)
const double = computed(() => count.value * 2)
// watch处理计算属性
watch(double, (value) => {
console.log('double changed:', value)
})
// watchEffect自动追踪计算属性
watchEffect(() => {
console.log('double in effect:', double.value)
})
异步场景下的行为差异
在异步回调中访问响应式数据时,watchEffect会自动追踪这些异步访问,而watch的回调中访问的响应式数据不会被自动追踪。
const data = ref(null)
// watch不会追踪异步回调中的访问
watch(data, async (newVal) => {
// 这里的otherRef.value不会被追踪
const result = await fetch('/api?param=' + otherRef.value)
})
// watchEffect会追踪异步操作中的访问
watchEffect(async () => {
// 自动追踪otherRef.value
const result = await fetch('/api?param=' + otherRef.value)
})
与组件生命周期的关系
在组件setup函数中使用时,两者都会在组件卸载时自动停止。但在某些情况下需要手动管理它们的生命周期。
import { onUnmounted } from 'vue'
export default {
setup() {
// 组件卸载时自动停止
watchEffect(() => { /* ... */ })
// 需要提前停止的情况
const stopHandle = watchEffect(() => { /* ... */ })
onUnmounted(() => stopHandle())
}
}
响应式依赖变化时的处理
当watch的依赖项发生变化时,Vue会比较新旧值是否相同来决定是否执行回调。watchEffect没有这种比较,只要依赖变化就会重新执行整个函数。
const obj = reactive({ a: 1 })
// watch会进行值比较
watch(() => obj.a, (newVal, oldVal) => {
// 只有obj.a实际变化时才会执行
})
// watchEffect无条件重新执行
watchEffect(() => {
// 只要obj.a被访问过,任何变化都会导致重新执行
console.log(obj.a)
})
与渲染系统的交互
watchEffect的执行可能影响组件渲染,因为默认情况下它会在组件更新前执行。可以通过flush: 'post'选项将其推迟到渲染之后。
// 可能影响DOM读取的示例
watchEffect(() => {
// 此时DOM可能还未更新
console.log(document.getElementById('test').textContent)
})
// 安全的DOM读取方式
watchEffect(
() => {
// DOM已经更新
console.log(document.getElementById('test').textContent)
},
{ flush: 'post' }
)
类型系统支持
在TypeScript中,watch能提供更精确的类型推断,因为它的依赖源是显式声明的。watchEffect的类型推断相对宽松。
const count = ref(0)
// watch有明确的参数类型
watch(count, (newVal: number, oldVal: number) => {
// ...
})
// watchEffect依赖上下文推断
watchEffect(() => {
const value = count.value // 自动推断为number
})
错误处理机制
两者都支持异步错误捕获,但watch的错误处理可以在回调中进行,而watchEffect需要在外部使用try/catch或onErrorCaptured。
// watch的错误处理
watch(errorProneRef, async (newVal) => {
try {
await doSomething(newVal)
} catch (err) {
console.error(err)
}
})
// watchEffect的错误处理
const stop = watchEffect(async (onCleanup) => {
try {
await doSomething()
} catch (err) {
console.error(err)
}
})
批量更新处理
在同一个tick中的多次响应式变化,watch和watchEffect都会合并处理,避免不必要的重复执行。
const a = ref(0)
const b = ref(0)
// 批量更新示例
watchEffect(() => {
console.log(a.value + b.value)
})
// 同一tick中的多次修改只会触发一次effect
a.value++
b.value++
与Vuex/Pinia的配合使用
在状态管理库中使用时,watch更适合监听特定的状态片段,而watchEffect适合响应状态变化的复杂副作用。
import { useStore } from 'pinia'
const store = useStore()
// watch监听特定状态
watch(
() => store.user.id,
(newId) => {
// ...
}
)
// watchEffect响应状态变化
watchEffect(() => {
if (store.isLoggedIn) {
// 自动追踪所有使用的store属性
fetchData(store.user.id)
}
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:计算属性computed的实现
下一篇:响应式系统的性能优化手段