The difference and implementation between watch and watchEffect
Basic Concepts of watch and watchEffect
Both watch
and watchEffect
are APIs in Vue 3 for reactive data observation, built on the same reactive system but with significant differences in usage and internal implementation. watch
requires explicit specification of the data source and callback function, while watchEffect
automatically tracks reactive dependencies accessed within it.
// watch example
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// watchEffect example
const state = reactive({ price: 100, quantity: 2 })
watchEffect(() => {
console.log(`Total: ${state.price * state.quantity}`)
})
Differences in Dependency Collection Mechanism
watch
performs dependency collection during initialization, requiring explicit specification of observation targets. Vue converts these targets into getter functions and establishes dependencies during component rendering. In contrast, watchEffect
employs immediate dependency collection, dynamically establishing dependencies while executing the side-effect function.
// watch dependencies are statically specified
watch(
[() => obj.a, () => obj.b],
([a, b], [prevA, prevB]) => {
/* ... */
}
)
// watchEffect dependencies are dynamically collected
watchEffect(() => {
// Only actually accessed properties are tracked
if (condition.value) {
console.log(obj.a)
} else {
console.log(obj.b)
}
})
Execution Timing and Scheduling Control
watch
is lazy by default, triggering the callback only when dependencies change, with asynchronous execution by default. watchEffect
executes immediately once and asynchronously on subsequent dependency changes. Both support scheduling control via options.flush
.
// watch's lazy execution feature
const data = ref(null)
watch(data, (newVal) => {
// Does not execute immediately; only triggers when data changes
})
// watchEffect's immediate execution feature
watchEffect(() => {
// Executes immediately once, then again when data changes
console.log(data.value)
})
// Scheduling control example
watchEffect(
() => { /* ... */ },
{
flush: 'post', // Execute after component updates
onTrack(e) { debugger }, // Debug hooks
onTrigger(e) { debugger }
}
)
Source Code Implementation Comparison
In Vue's source code, both watch
and watchEffect
implement core logic via the doWatch
function but differ in parameter handling. watch
first normalizes the source into a getter function, while watchEffect
directly uses the passed function.
// Simplified implementation from 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
) {
// Normalize source into a getter function
let getter: () => any
if (isFunction(source)) {
getter = () => source()
} else {
getter = () => traverse(source)
}
// Cleanup function handling
let cleanup: () => void
const onCleanup = (fn: () => void) => {
cleanup = runner.onStop = () => fn()
}
// Scheduler implementation
const scheduler = () => {
if (!runner.active) return
if (cb) {
// watch handling logic
const newValue = runner()
if (hasChanged(newValue, oldValue)) {
callWithAsyncErrorHandling(cb, [
newValue,
oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect handling logic
runner()
}
}
// Create reactive effect
const runner = effect(getter, {
lazy: true,
scheduler,
onTrack: options.onTrack,
onTrigger: options.onTrigger
})
// Initial execution logic
if (cb) {
oldValue = runner()
} else {
runner()
}
}
Stopping Observation and Side Effect Cleanup
Both return a stop function to cancel observation. watchEffect
is more commonly used for scenarios requiring automatic side effect cleanup via onCleanup
.
// Stop observation example
const stop = watchEffect(() => { /* ... */ })
stop() // Cancel observation
// Side effect cleanup example
watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('Running')
}, 1000)
onCleanup(() => clearInterval(timer))
})
Performance Optimization and Use Cases
watch
is suitable for precisely observing specific data changes, especially when access to old values is needed. watchEffect
is ideal for complex dependency relationships or scenarios requiring automatic tracking of all accessed reactive properties.
// Suitable for watch
watch(
() => route.params.id,
(newId, oldId) => {
fetchData(newId)
}
)
// Suitable for watchEffect
watchEffect(() => {
// Automatically tracks all used reactive properties
document.title = `${user.name} - ${page.title}`
})
Deep Observation and Immediate Execution
watch
supports deep observation of objects and arrays via deep: true
. watchEffect
automatically tracks all accessed properties without requiring separate deep observation configuration.
// watch deep observation
const obj = reactive({ nested: { count: 0 } })
watch(
() => obj,
(newVal) => {
console.log('nested changed', newVal.nested.count)
},
{ deep: true }
)
// watchEffect automatic deep tracking
watchEffect(() => {
// Any level of access is tracked
console.log(obj.nested.count)
})
Debugging Capabilities Comparison
Both support onTrack
and onTrigger
debugging hooks, but watchEffect
may be more complex to debug due to its automatic dependency collection. watch
's explicit dependency declaration makes debugging more straightforward.
watchEffect(
() => { /* effect */ },
{
onTrack(e) {
debugger // Triggers when a dependency is tracked
},
onTrigger(e) {
debugger // Triggers when a dependency change triggers the effect
}
}
)
Collaboration with Reactive APIs
watch
and watchEffect
work consistently with other Vue 3 reactive APIs like ref
, reactive
, and computed
, but watchEffect
handles computed properties with subtle differences.
const count = ref(0)
const double = computed(() => count.value * 2)
// watch handles computed properties
watch(double, (value) => {
console.log('double changed:', value)
})
// watchEffect automatically tracks computed properties
watchEffect(() => {
console.log('double in effect:', double.value)
})
Behavioral Differences in Asynchronous Scenarios
When accessing reactive data in asynchronous callbacks, watchEffect
automatically tracks these asynchronous accesses, while watch
does not track reactive data accessed in its callback.
const data = ref(null)
// watch does not track accesses in async callbacks
watch(data, async (newVal) => {
// otherRef.value here is not tracked
const result = await fetch('/api?param=' + otherRef.value)
})
// watchEffect tracks accesses in async operations
watchEffect(async () => {
// Automatically tracks otherRef.value
const result = await fetch('/api?param=' + otherRef.value)
})
Relationship with Component Lifecycle
When used in a component's setup
function, both automatically stop when the component unmounts. However, manual lifecycle management may be required in some cases.
import { onUnmounted } from 'vue'
export default {
setup() {
// Automatically stops on component unmount
watchEffect(() => { /* ... */ })
// Cases requiring early stopping
const stopHandle = watchEffect(() => { /* ... */ })
onUnmounted(() => stopHandle())
}
}
Handling Reactive Dependency Changes
When watch
dependencies change, Vue compares old and new values to decide whether to execute the callback. watchEffect
lacks this comparison and re-executes the entire function whenever dependencies change.
const obj = reactive({ a: 1 })
// watch performs value comparison
watch(() => obj.a, (newVal, oldVal) => {
// Executes only when obj.a actually changes
})
// watchEffect unconditionally re-executes
watchEffect(() => {
// Any change to obj.a triggers re-execution if accessed
console.log(obj.a)
})
Interaction with the Rendering System
watchEffect
execution may affect component rendering since it executes before component updates by default. The flush: 'post'
option can defer execution until after rendering.
// Example that may affect DOM reading
watchEffect(() => {
// DOM may not be updated yet
console.log(document.getElementById('test').textContent)
})
// Safe DOM reading approach
watchEffect(
() => {
// DOM is already updated
console.log(document.getElementById('test').textContent)
},
{ flush: 'post' }
)
Type System Support
In TypeScript, watch
provides more precise type inference due to its explicit dependency declaration. watchEffect
type inference is more relaxed.
const count = ref(0)
// watch has explicit parameter types
watch(count, (newVal: number, oldVal: number) => {
// ...
})
// watchEffect relies on contextual inference
watchEffect(() => {
const value = count.value // Automatically inferred as number
})
Error Handling Mechanisms
Both support asynchronous error capture, but watch
error handling can be done in the callback, while watchEffect
requires external try/catch
or onErrorCaptured
.
// watch error handling
watch(errorProneRef, async (newVal) => {
try {
await doSomething(newVal)
} catch (err) {
console.error(err)
}
})
// watchEffect error handling
const stop = watchEffect(async (onCleanup) => {
try {
await doSomething()
} catch (err) {
console.error(err)
}
})
Batch Update Handling
For multiple reactive changes in the same tick, both watch
and watchEffect
merge processing to avoid unnecessary repeated execution.
const a = ref(0)
const b = ref(0)
// Batch update example
watchEffect(() => {
console.log(a.value + b.value)
})
// Multiple modifications in the same tick trigger the effect only once
a.value++
b.value++
Integration with Vuex/Pinia
When used with state management libraries, watch
is better for observing specific state fragments, while watchEffect
suits complex side effects responding to state changes.
import { useStore } from 'pinia'
const store = useStore()
// watch observes specific state
watch(
() => store.user.id,
(newId) => {
// ...
}
)
// watchEffect responds to state changes
watchEffect(() => {
if (store.isLoggedIn) {
// Automatically tracks all used store properties
fetchData(store.user.id)
}
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:计算属性computed的实现
下一篇:响应式系统的性能优化手段