阿里云主机折上折
  • 微信号
Current Site:Index > The difference and implementation between watch and watchEffect

The difference and implementation between watch and watchEffect

Author:Chuan Chen 阅读数:33065人阅读 分类: Vue.js

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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.