Vue3 watch 与 watchEffect 深度解析 🚀
在 Vue3 的 Composition API 中,
watch和watchEffect是处理响应式数据变化的核心工具。本文将深入对比两者的异同,帮助你写出更优雅的响应式代码!
为什么需要监听?
在 Vue 应用中,我们经常需要:
- 🔄 当数据变化时执行特定操作
- 💾 数据变化时自动保存状态
- 📡 监听路由参数变化获取数据
- 🎯 响应表单输入进行搜索/验证
Vue3 提供了两个强大的 API 来实现这些需求:watch 和 watchEffect。
🔰 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 技巧
有任何问题或建议,欢迎在评论区留言!👇
相关推荐: