阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > watch与watchEffect的区别与实现

watch与watchEffect的区别与实现

作者:陈川 阅读数:33066人阅读 分类: Vue.js

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌