Vue 3 watchEffect:如何实现响应式依赖的自动追踪与副作用管理?

17 阅读17分钟

Vue 3 watchEffect 深度解析——自动追踪响应式依赖的便捷API

一、watchEffect 基本概念

1.1 什么是watchEffect

watchEffect 是 Vue 3 提供的一个强大的响应式副作用监听API,它能够自动追踪回调函数中使用的响应式依赖,并在这些依赖发生变化时重新执行回调函数。与传统的 watch 相比,watchEffect 无需手动指定监听的数据源,大大简化了代码编写。

1.2 watchEffect 与 watch 的区别

特性watchwatchEffect
依赖指定需要手动指定监听的数据源自动追踪回调中使用的响应式依赖
执行时机默认懒执行,仅在数据源变化时触发立即执行一次,之后依赖变化时重新执行
参数获取可以获取新旧值无法直接获取新旧值,只能访问当前值
使用场景精确监听特定数据源变化处理多个依赖的复杂副作用场景

1.3 watchEffect 的自动追踪机制

watchEffect 的核心优势在于其自动依赖追踪能力。当回调函数执行时,Vue 会记录所有被访问的响应式属性,当这些属性发生变化时,会自动重新执行回调函数。

graph TD
    A[组件初始化] --> B[执行watchEffect回调]
    B --> C[Vue追踪所有访问的响应式依赖]
    C --> D[依赖变化]
    D --> E[执行清理函数(如果存在)]
    E --> F[重新执行watchEffect回调]
    F --> C

二、watchEffect 核心特性

2.1 立即执行

watchEffect 会在组件初始化时立即执行一次回调函数,这使得它非常适合用于初始化数据请求或DOM操作。

<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)

// 立即执行,之后count变化时重新执行
watchEffect(() => {
  console.log(`Count is: ${count.value}`)
})
</script>

2.2 自动依赖追踪

watchEffect 会自动追踪回调函数中使用的所有响应式依赖,无需手动指定监听源。

<script setup>
import { reactive, watchEffect } from 'vue'

const user = reactive({
  name: 'John',
  age: 30
})

// 自动追踪user.name和user.age的变化
watchEffect(() => {
  console.log(`User: ${user.name}, Age: ${user.age}`)
})
</script>

2.3 副作用清理

当 watchEffect 重新执行或组件卸载时,我们可以通过清理函数来处理副作用,比如取消网络请求、清除定时器等。

<script setup>
import { ref, watchEffect } from 'vue'

const todoId = ref(1)
const todoData = ref(null)

watchEffect((onCleanup) => {
  const controller = new AbortController()
  
  // 发起网络请求
  fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`, {
    signal: controller.signal
  })
  .then(response => response.json())
  .then(data => todoData.value = data)
  
  // 清理函数:当依赖变化时取消之前的请求
  onCleanup(() => {
    controller.abort()
  })
})
</script>

2.4 执行时机控制

watchEffect 提供了三种执行时机选项:

  • pre:默认值,在组件更新前执行
  • post:在组件更新后执行,适合访问更新后的DOM
  • sync:同步执行,每次依赖变化立即触发
<script setup>
import { ref, watchEffect, watchPostEffect, watchSyncEffect } from 'vue'

const count = ref(0)

// 默认时机:组件更新前执行
watchEffect(() => {
  console.log('Default watchEffect')
})

// 组件更新后执行
watchPostEffect(() => {
  console.log('Post watchEffect')
})

// 同步执行
watchSyncEffect(() => {
  console.log('Sync watchEffect')
})
</script>

三、watchEffect 实战应用场景

3.1 数据请求与状态同步

往期文章归档
免费好用的热门在线工具

watchEffect 非常适合处理数据请求场景,特别是当请求依赖多个响应式参数时。

<script setup>
import { reactive, ref, watchEffect } from 'vue'

const filters = reactive({
  keyword: '',
  category: 'all'
})
const products = ref([])
const loading = ref(false)

watchEffect(async (onCleanup) => {
  loading.value = true
  
  // 构建请求URL
  const url = new URL('https://api.example.com/products')
  url.searchParams.append('keyword', filters.keyword)
  url.searchParams.append('category', filters.category)
  
  const controller = new AbortController()
  
  try {
    const response = await fetch(url, { signal: controller.signal })
    products.value = await response.json()
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Failed to fetch products:', error)
    }
  } finally {
    loading.value = false
  }
  
  onCleanup(() => {
    controller.abort()
  })
})
</script>

3.2 DOM 操作与动画控制

当需要根据响应式状态动态操作DOM时,watchEffect 可以确保DOM操作在状态变化时及时执行。

<script setup>
import { ref, watchEffect } from 'vue'

const isModalOpen = ref(false)
const modalElement = ref(null)

watchEffect(() => {
  if (isModalOpen.value && modalElement.value) {
    // 显示模态框并添加动画
    modalElement.value.style.display = 'block'
    setTimeout(() => {
      modalElement.value.classList.add('open')
    }, 10)
  } else if (modalElement.value) {
    // 隐藏模态框并移除动画
    modalElement.value.classList.remove('open')
    setTimeout(() => {
      modalElement.value.style.display = 'none'
    }, 300)
  }
})
</script>

<template>
  <div ref="modalElement" class="modal">
    <!-- 模态框内容 -->
  </div>
</template>

3.3 复杂状态组合监听

当需要监听多个状态的组合变化时,watchEffect 可以自动追踪所有相关依赖,无需手动维护依赖列表。

<script setup>
import { reactive, watchEffect } from 'vue'

const form = reactive({
  username: '',
  password: '',
  confirmPassword: ''
})
const isFormValid = ref(false)

watchEffect(() => {
  // 自动追踪form中的所有属性
  const hasUsername = form.username.trim().length > 0
  const hasPassword = form.password.trim().length >= 6
  const passwordsMatch = form.password === form.confirmPassword
  
  isFormValid.value = hasUsername && hasPassword && passwordsMatch
})
</script>

四、高级用法与最佳实践

4.1 结合 ref 和 reactive 使用

watchEffect 可以无缝结合 ref 和 reactive 使用,自动追踪所有响应式依赖。

<script setup>
import { ref, reactive, watchEffect } from 'vue'

const count = ref(0)
const user = reactive({
  name: 'John',
  age: 30
})

watchEffect(() => {
  console.log(`Count: ${count.value}, User: ${user.name}`)
})
</script>

4.2 停止 watchEffect

在某些情况下,我们可能需要手动停止 watchEffect,比如在条件满足时停止监听。

<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
const stopWatcher = ref(null)

stopWatcher.value = watchEffect(() => {
  console.log(`Count: ${count.value}`)
  
  // 当count达到10时停止监听
  if (count.value >= 10) {
    stopWatcher.value()
  }
})
</script>

4.3 性能优化技巧

  • 避免在回调中执行昂贵操作:如果必须执行,考虑使用防抖或节流
  • 限制依赖范围:只在回调中访问必要的响应式属性
  • 使用 watchPostEffect:如果需要访问更新后的DOM,使用 watchPostEffect 代替默认时机

五、课后 Quiz

问题1:watchEffect 和 watch 的主要区别是什么?

答案解析

  • watch 需要手动指定监听的数据源,而 watchEffect 自动追踪回调中使用的响应式依赖
  • watch 默认懒执行,仅在数据源变化时触发,而 watchEffect 立即执行一次,之后依赖变化时重新执行
  • watch 可以获取新旧值,而 watchEffect 无法直接获取新旧值
  • watch 适合精确监听特定数据源,而 watchEffect 适合处理多个依赖的复杂副作用场景

问题2:如何在 watchEffect 中清理副作用?

答案解析: 可以通过两种方式清理副作用:

  1. 使用 onCleanup 参数:watchEffect 的回调函数接收一个 onCleanup 函数作为参数,调用它并传入清理逻辑
  2. 使用 onWatcherCleanup API:在 Vue 3.5+ 中,可以使用 onWatcherCleanup API 注册清理函数

示例代码:

watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('Timeout executed')
  }, 1000)
  
  onCleanup(() => {
    clearTimeout(timer)
  })
})

问题3:watchEffect 的执行时机有哪些选项?

答案解析: watchEffect 有三种执行时机选项:

  • pre:默认值,在组件更新前执行
  • post:在组件更新后执行,适合访问更新后的DOM
  • sync:同步执行,每次依赖变化立即触发

可以通过以下方式指定:

watchEffect(() => {
  // 逻辑
}, { flush: 'post' })

// 或者使用别名
watchPostEffect(() => {
  // 逻辑
})

六、常见报错解决方案

6.1 依赖未被追踪

报错现象:watchEffect 没有在依赖变化时重新执行 原因分析

  • 回调中访问的不是响应式属性
  • 在异步回调中访问响应式属性,导致依赖未被追踪 解决办法
  • 确保所有需要追踪的属性都是响应式的(使用 ref 或 reactive 创建)
  • 避免在异步回调中访问响应式属性,或者将异步操作放在同步代码之前

6.2 异步回调中的依赖问题

报错现象:异步回调中的响应式属性变化不会触发 watchEffect 重新执行 原因分析:watchEffect 只在同步执行阶段追踪依赖,异步回调中的依赖不会被追踪 解决办法

  • 将需要追踪的依赖在同步代码中提前访问
  • 使用 watch 代替 watchEffect,手动指定需要监听的数据源

6.3 内存泄漏问题

报错现象:组件卸载后,watchEffect 仍然在执行 原因分析

  • 没有清理副作用(如定时器、网络请求等)
  • 异步创建的 watchEffect 没有手动停止 解决办法
  • 使用清理函数清理副作用
  • 对于异步创建的 watchEffect,手动调用停止函数
  • 确保所有 watchEffect 都是同步创建的,这样会在组件卸载时自动停止

七、参考链接

参考链接:vuejs.org/guide/essen…