Vue3 watch 与 watchEffect 深度解析

Vue3 watch 与 watchEffect 深度解析 🚀

在 Vue3 的 Composition API 中,watchwatchEffect 是处理响应式数据变化的核心工具。本文将深入对比两者的异同,帮助你写出更优雅的响应式代码!


为什么需要监听?

在 Vue 应用中,我们经常需要:

  • 🔄 当数据变化时执行特定操作
  • 💾 数据变化时自动保存状态
  • 📡 监听路由参数变化获取数据
  • 🎯 响应表单输入进行搜索/验证

Vue3 提供了两个强大的 API 来实现这些需求:watchwatchEffect


🔰 watchEffect - 响应式依赖自动追踪

什么是 watchEffect?

watchEffect 是一个立即执行 的函数,它会自动追踪回调函数中使用的所有响应式依赖。当这些依赖发生变化时,回调函数会自动重新执行。

javascript 复制代码
import { watchEffect, ref } from 'vue'

const count = ref(0)
const name = ref('张三')

watchEffect(() => {
    console.log(`count: ${count.value}, name: ${name.value}`)
})
// 立即输出: count: 0, name: 张三
// 当 count 或 name 变化时会自动输出

核心特性

特性 说明
立即执行 副作用函数在组件初始化时立即执行一次
🔍 自动追踪 自动追踪回调中使用的所有响应式数据
🎯 无需指定 不需要显式指定要监听的对象
🔗 深度追踪 自动追踪深层对象的变化

清除副作用

javascript 复制代码
watchEffect((onInvalidate) => {
    const timer = setInterval(() => {
        console.log('定时任务执行中...')
    }, 1000)

    // 组件卸载或重新运行时清除
    onInvalidate(() => {
        clearInterval(timer)
        console.log('定时任务已清除')
    })
})

适用场景

最佳实践场景:

javascript 复制代码
// 1. 日志记录
watchEffect(() => {
    console.log('用户操作:', userAction.value)
})

// 2. 数据同步(如 localStorage)
watchEffect(() => {
    localStorage.setItem('theme', theme.value)
})

// 3. 同时监听多个相关数据
watchEffect(() => {
    const fullName = `${firstName.value} ${lastName.value}`
    console.log('全名更新:', fullName)
})

// 4. 组件初始化时的数据获取
watchEffect(async () => {
    const data = await fetchData(id.value)
    result.value = data
})

🔰 watch - 精确数据源监听

什么是 watch?

watch 用于监听特定的数据源 ,并在数据变化时执行回调函数。需要显式指定要监听的数据源。

javascript 复制代码
import { watch, ref } from 'vue'

const count = ref(0)

watch(count, (newValue, oldValue) => {
    console.log(`count 从 ${oldValue} 变化到 ${newValue}`)
})

监听多种数据源

javascript 复制代码
const count = ref(0)
const name = ref('张三')

// 同时监听多个数据源
watch([count, name], ([newCount, oldCount], [newName, oldName]) => {
    console.log(`count: ${oldCount} → ${newCount}`)
    console.log(`name: ${oldName} → ${newName}`)
})

深度监听

javascript 复制代码
const user = ref({
    name: '张三',
    info: {
        age: 18,
        city: '北京'
    }
})

// 深度监听对象变化
watch(user, (newValue, oldValue) => {
    console.log('用户信息变化了')
}, { deep: true })

// 或者使用 getter 函数监听特定属性
watch(() => user.value.info.age, (newAge) => {
    console.log(`年龄变为: ${newAge}`)
})

立即执行

javascript 复制代码
const searchQuery = ref('')

watch(searchQuery, (value) => {
    console.log(`搜索: ${value}`)
    fetchResults(value)
}, { immediate: true })  // 立即执行,初始值为 ""

📊 核心对比

对比项 watch watchEffect
监听方式 显式指定数据源 自动追踪依赖
立即执行 ❌ 默认不执行,需设置 immediate ✅ 默认立即执行
获取旧值 ✅ 完整支持 ❌ 不支持
深度监听 ✅ 支持 { deep: true } 🔄 自动追踪深层变化
性能 ⚡ 更精确,只监听指定数据 ⚠️ 可能包含无关依赖
调试友好 ✅ 清晰知道监听目标 ⚠️ 依赖较隐式

🔥 何时选择哪个?

复制代码
选择 watch ✅
├── 需要获取变化前后的值
├── 只想监听特定的数据
├── 需要配置选项(deep、immediate)
└── 需要停止监听时返回 unwatch 函数

选择 watchEffect ✅
├── 副作用操作(日志、同步等)
├── 不知道具体要监听哪些数据
├── 需要同时追踪多个相关数据
└── 组件初始化时需要执行

💡 实战技巧

1. 监听计算属性

javascript 复制代码
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

const fullName = computed(() => `${firstName.value}${lastName.value}`)

// 监听计算属性
watch(fullName, (newVal, oldVal) => {
    console.log(`姓名: ${oldVal} → ${newVal}`)
})

2. 监听 DOM 元素

javascript 复制代码
import { ref, watch, nextTick } from 'vue'

const inputRef = ref(null)

watch(() => inputRef.value, (el) => {
    if (el) {
        el.focus()
    }
}, { immediate: true })

3. 防抖处理

javascript 复制代码
import { watch } from 'vue'
import { debounce } from 'lodash-es'

const searchQuery = ref('')

// 防抖搜索
watch(searchQuery, debounce((value) => {
    console.log(`搜索: ${value}`)
    fetchSearchResults(value)
}, 300))

4. 监听路由变化

javascript 复制代码
import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

watch(() => route.params.id, (newId) => {
    if (newId) {
        fetchArticle(newId)
    }
})

5. 停止监听

javascript 复制代码
import { onUnmounted, watch } from 'vue'

const count = ref(0)

const stop = watch(count, (value) => {
    console.log(value)
})

// 方式1: 手动停止
stop()

// 方式2: 组件卸载时自动停止
onUnmounted(() => {
    stop()
})

// 方式3: 只监听一次
watch(count, (value) => {
    console.log(value)
}, { once: true })

⚠️ 常见问题

Q1: watch 和 watchEffect 哪个性能更好?

答: 通常 watch 性能更好。

  • watch 精确指定监听目标,只在指定数据变化时触发
  • watchEffect 追踪所有依赖,可能包含不必要的数据
javascript 复制代码
// 推荐: 只监听实际需要的数据
watch(userId, async (id) => {
    const data = await fetchUser(id)
    // ...
})

// 不推荐: 可能会追踪多余的依赖
watchEffect(async () => {
    const data = await fetchUser(userId.value)
    // ...
})

Q2: 为什么 watchEffect 不能获取旧值?

答: 这是设计理念的差异。

  • watchEffect 关注的是副作用,回答"现在是什么状态"而非"如何变成这样"
  • watch 关注的是变化本身,回答"从 X 变成 Y 的过程"
javascript 复制代码
// watchEffect: 副作用思维
watchEffect(() => {
    document.title = `count: ${count.value}`
})

// watch: 变化思维
watch(count, (newVal, oldVal) => {
    console.log(`${oldVal} → ${newVal}`)
})

Q3: watch 监听不到数组的变化?

答: 区分两种情况:

javascript 复制代码
const arr = ref([1, 2, 3])

// ❌ 直接修改索引,不触发(需要 deep)
arr.value[0] = 10

// ✅ 替换整个数组,触发
arr.value = [10, 2, 3]

// ✅ 或使用 push/splice 等方法,触发
arr.value.push(4)

// ✅ 如需监听深层变化
watch(arr, callback, { deep: true })

Q4: watchEffect 中修改被监听的值会怎样?

答: 会导致无限循环!🚨

javascript 复制代码
const count = ref(0)

// ❌ 错误: 无限循环
watchEffect(() => {
    count.value += 1  // 修改被追踪的值
})

// ✅ 正确: 使用 watch 代替
watch(count, (value) => {
    // 可以在这里修改其他值
})

Q5: 如何监听多个嵌套属性的变化?

javascript 复制代码
const user = ref({
    profile: {
        name: '张三',
        age: 18
    }
})

// 方式1: getter 函数
watch(() => user.value.profile.name, (newName) => {
    console.log('名字变化:', newName)
})

// 方式2: deep watch
watch(() => ({ ...user.value.profile }), (newProfile) => {
    console.log('profile 变化:', newProfile)
}, { deep: true })

📝 最佳实践总结

✅ 推荐写法

javascript 复制代码
// 1. 需要旧值时使用 watch
watch(data, (newVal, oldVal) => {
    // 对比新旧值
})

// 2. 副作用操作使用 watchEffect
watchEffect(() => {
    localStorage.setItem('data', JSON.stringify(data.value))
})

// 3. 精准监听,使用 getter 函数
watch(() => obj.value.nested.prop, callback)

// 4. 清理副作用
watchEffect((onInvalidate) => {
    const subscription = subscribeToData(id.value)
    onInvalidate(() => subscription.unsubscribe())
})

❌ 避免写法

javascript 复制代码
// 1. 避免在 watchEffect 中修改被追踪的值
watchEffect(() => {
    count.value++  // ❌ 无限循环
})

// 2. 避免对大对象使用 deep: true
watch(hugeObject, callback, { deep: true })  // ⚠️ 性能问题

// 3. 避免在 watch 中遗漏 immediate 如果需要初始化
watch(data, callback)  // ❌ 首次不执行

🎯 实战案例:搜索功能

javascript 复制代码
import { ref, watch, watchEffect } from 'vue'

// 防抖搜索 - 使用 watch
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)

watch(searchQuery, async (query) => {
    if (!query.trim()) {
        searchResults.value = []
        return
    }
    
    isLoading.value = true
    try {
        const results = await api.search(query)
        searchResults.value = results
    } finally {
        isLoading.value = false
    }
}, { 
    immediate: true,
    debounce: 300  // 实际项目中需要用 lodash debounce
})

// 搜索历史 - 使用 watchEffect
const searchHistory = ref([])

watchEffect((onInvalidate) => {
    // 监听搜索结果,保存历史
    if (searchResults.value.length > 0) {
        const timer = setTimeout(() => {
            saveToHistory(searchQuery.value)
        }, 1000)
        
        onInvalidate(() => clearTimeout(timer))
    }
})

📚 总结

API 特点 使用场景
watch 精确监听、获取旧值、可配置 需要对比变化、数据验证、精确控制
watchEffect 自动追踪、立即执行、简洁 副作用操作、初始化逻辑、多数据联动

选择原则:

  • 只需要副作用 → watchEffect
  • 需要旧值或精确控制 → watch
  • 监听多个数据变化 → 两者皆可,按需选择

💬 写在最后

如果你觉得这篇文章对你有帮助,欢迎:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 💬 评论区交流心得
  • 👤 关注我获取更多 Vue3 技巧

有任何问题或建议,欢迎在评论区留言!👇


相关推荐:

相关推荐
狂师1 小时前
测试工程师的AI 技能库:推荐5个让你效率翻倍的Skills
前端·后端·测试
CodeSheep1 小时前
DeepSeek正式官宣摇人,夯!
前端·后端·程序员
用户059540174461 小时前
Redis持久化踩坑实录:这个数据丢失Bug让我排查了6小时
前端·css
用户2136610035721 小时前
VueRouter进阶-动态路由与嵌套路由
前端·vue.js
梯度不陡1 小时前
Signal #17:Agent 开始进入组织系统
前端·javascript
何智超1 小时前
AI 微前端性能优化之旅(上):复盘
前端·vibecoding
许我半盏清茶1 小时前
前端路由:理解 hash 路由和 history 路由原理
前端·react.js
胡萝卜术1 小时前
从暴力到Z字形消元:力扣240「搜索二维矩阵II」的降维打击之路
前端·javascript·面试
比老马还六2 小时前
Bipes-Blockly项目二次开发/Coze智能体(十)
前端·嵌入式