侦听器watch与watchEffect
在Vue 3中,侦听器是用于监听响应式数据变化并执行副作用的API。Vue 3提供了watch和watchEffect两个主要的侦听器API。本集我们将深入探讨这两个API的使用方法、实现原理和最佳实践。
一、侦听器的基本概念
1. 什么是侦听器?
侦听器是一种用于监听响应式数据变化并执行副作用的机制。当监听的响应式数据发生变化时,侦听器会自动执行指定的回调函数。
2. 侦听器的作用
- 执行副作用:当数据变化时执行异步或开销较大的操作
- 数据同步:将一个数据的变化同步到另一个数据
- 状态管理:监听状态变化,执行相应的逻辑
- API调用:当数据变化时调用API
- DOM操作:当数据变化时操作DOM
3. watch与watchEffect的区别
| 特性 | watch |
watchEffect |
|---|---|---|
| 依赖追踪 | 手动指定依赖 | 自动追踪依赖 |
| 执行时机 | 依赖变化时执行 | 初始执行一次,依赖变化时执行 |
| 回调参数 | 可以访问新值和旧值 | 无法直接访问旧值 |
| 控制执行 | 可以更精细地控制何时执行 | 自动执行,控制较少 |
| 适用场景 | 需要访问旧值或精细控制 | 只需执行副作用,无需访问旧值 |
二、watch API的基本使用
1. 监听单个ref
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})2. 监听reactive对象
import { reactive, watch } from 'vue'
const user = reactive({
name: 'Alice',
age: 30
})
watch(user, (newUser, oldUser) => {
console.log('User changed:', newUser, oldUser)
}) // 注意:此处oldUser是新值的引用,不是真正的旧值
// 监听reactive对象的单个属性
watch(() => user.name, (newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`)
})3. 监听多个数据源
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('Hello')
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`Count: ${oldCount} -> ${newCount}`)
console.log(`Message: ${oldMessage} -> ${newMessage}`)
})4. 监听深度变化
import { reactive, watch } from 'vue'
const user = reactive({
name: 'Alice',
address: {
city: 'Beijing',
street: 'Main St'
}
})
// 深度监听
watch(
() => user.address,
(newAddress, oldAddress) => {
console.log('Address changed:', newAddress, oldAddress)
},
{ deep: true } // 开启深度监听
)5. 立即执行
import { ref, watch } from 'vue'
const count = ref(0)
watch(
count,
(newValue, oldValue) => {
console.log(`Count: ${oldValue} -> ${newValue}`)
},
{ immediate: true } // 立即执行一次
)三、watchEffect API的基本使用
1. 基本用法
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log(`Count is: ${count.value}`)
}) // 初始执行一次,输出:Count is: 0
count.value++ // 依赖变化,执行副作用,输出:Count is: 12. 访问DOM
import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(() => {
// 访问DOM元素
document.title = `Count: ${count.value}`
})3. 异步操作
import { ref, watchEffect } from 'vue'
const searchQuery = ref('')
watchEffect(async () => {
console.log(`Searching for: ${searchQuery.value}`)
const result = await fetch(`/api/search?q=${searchQuery.value}`)
const data = await result.json()
console.log('Search result:', data)
})四、侦听器的实现原理
1. watch的实现原理
watch的实现基于Vue 3的响应式系统,主要包括以下步骤:
- 初始化:注册监听器,指定要监听的数据源和回调函数
- 依赖收集:当监听函数执行时,Vue会自动收集依赖的响应式数据
- 依赖变化:当依赖的响应式数据变化时,触发更新
- 执行回调:执行监听器的回调函数,传递新值和旧值
2. watchEffect的实现原理
watchEffect的实现也基于Vue 3的响应式系统,主要包括以下步骤:
- 初始执行:立即执行副作用函数
- 依赖收集:在执行副作用函数过程中,Vue会自动收集依赖的响应式数据
- 依赖变化:当依赖的响应式数据变化时,触发副作用函数重新执行
- 重新执行:重新执行副作用函数,完成副作用更新
3. 简化的实现原理
// watch的简化实现
function watch(source, callback, options = {}) {
let getter
// 处理不同类型的source
if (typeof source === 'function') {
getter = source
} else if (isRef(source)) {
getter = () => source.value
} else if (isReactive(source)) {
getter = () => source
}
let oldValue
let newValue
let cleanup
// 副作用函数
const effect = () => {
newValue = getter()
// 如果有清理函数,先执行
if (cleanup) {
cleanup()
}
// 执行回调,传递新值和旧值
callback(newValue, oldValue, onCleanup => {
cleanup = onCleanup
})
// 更新旧值
oldValue = newValue
}
// 根据选项决定是否立即执行
if (options.immediate) {
effect()
} else {
oldValue = getter()
}
// 依赖收集,当依赖变化时执行effect
trackEffect(effect)
}
// watchEffect的简化实现
function watchEffect(effect, options = {}) {
let cleanup
// 包装副作用函数
const wrappedEffect = () => {
// 如果有清理函数,先执行
if (cleanup) {
cleanup()
}
// 执行副作用函数,传递清理函数
effect(onCleanup => {
cleanup = onCleanup
})
}
// 立即执行一次
wrappedEffect()
// 依赖收集,当依赖变化时执行wrappedEffect
trackEffect(wrappedEffect)
}五、清理副作用
1. watch的清理函数
import { ref, watch } from 'vue'
const searchQuery = ref('')
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
// 创建一个AbortController用于取消请求
const controller = new AbortController()
// 注册清理函数
onCleanup(() => {
controller.abort() // 取消之前的请求
console.log('Cleaned up previous request')
})
try {
const response = await fetch(`/api/search?q=${newQuery}`, {
signal: controller.signal
})
const data = await response.json()
console.log('Search result:', data)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search error:', error)
}
}
})2. watchEffect的清理函数
import { ref, watchEffect } from 'vue'
const searchQuery = ref('')
watchEffect(async (onCleanup) => {
// 创建一个AbortController用于取消请求
const controller = new AbortController()
// 注册清理函数
onCleanup(() => {
controller.abort() // 取消之前的请求
console.log('Cleaned up previous request')
})
try {
const response = await fetch(`/api/search?q=${searchQuery.value}`, {
signal: controller.signal
})
const data = await response.json()
console.log('Search result:', data)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search error:', error)
}
}
})六、侦听器的高级用法
1. 停止侦听器
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// 停止watch
const stopWatch = watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
// 停止watchEffect
const stopWatchEffect = watchEffect(() => {
console.log(`Count is: ${count.value}`)
})
// 停止侦听器
setTimeout(() => {
stopWatch() // 停止watch
stopWatchEffect() // 停止watchEffect
console.log('Listeners stopped')
}, 5000)2. 防抖与节流
import { ref, watch } from 'vue'
const searchQuery = ref('')
// 防抖函数
function debounce(fn, delay) {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}
}
// 带防抖的watch
watch(
searchQuery,
debounce(async (newQuery) => {
console.log(`Searching for: ${newQuery}`)
const result = await fetch(`/api/search?q=${newQuery}`)
const data = await result.json()
console.log('Search result:', data)
}, 300)
)3. 监听嵌套对象
import { reactive, watch } from 'vue'
const user = reactive({
profile: {
name: 'Alice',
address: {
city: 'Beijing',
street: 'Main St'
}
}
})
// 监听嵌套对象的单个属性
watch(
() => user.profile.address.city,
(newCity, oldCity) => {
console.log(`City changed from ${oldCity} to ${newCity}`)
}
)
// 深度监听嵌套对象
watch(
() => user.profile,
(newProfile, oldProfile) => {
console.log('Profile changed:', newProfile, oldProfile)
},
{ deep: true }
)4. 与computed结合使用
import { ref, computed, watch } from 'vue'
const radius = ref(5)
// 计算属性:计算圆的面积
const area = computed(() => {
return Math.PI * radius.value * radius.value
})
// watch:监听面积变化
watch(area, (newArea, oldArea) => {
console.log(`Area changed from ${oldArea} to ${newArea}`)
})七、侦听器的最佳实践
1. 选择合适的侦听器API
- 当需要访问旧值或精细控制监听行为时,使用
watch - 当只需执行副作用,无需访问旧值时,使用
watchEffect - 当需要初始执行一次副作用时,使用
watchEffect或带有immediate: true选项的watch
2. 避免在侦听器中修改响应式数据
// 不推荐:在侦听器中修改响应式数据,可能导致无限循环
const count = ref(0)
watch(count, (newValue) => {
if (newValue > 10) {
count.value = 10 // 可能导致无限循环
}
})
// 推荐:使用计算属性或其他方式处理
const count = ref(0)
const limitedCount = computed(() => Math.min(count.value, 10))3. 清理副作用
- 当执行异步操作时,一定要清理之前的异步操作
- 使用
onCleanup函数注册清理逻辑 - 清理函数会在副作用重新执行前或组件卸载时执行
4. 避免过度使用侦听器
- 只在必要时使用侦听器
- 对于简单的数据转换,使用计算属性
- 对于复杂的副作用,才使用侦听器
5. 注意性能
- 避免深度监听大型对象,这会导致性能问题
- 对于大型对象,只监听需要变化的特定属性
- 对于频繁变化的数据,使用防抖或节流
八、侦听器的常见错误
1. 监听reactive对象时无法获取旧值
import { reactive, watch } from 'vue'
const user = reactive({
name: 'Alice',
age: 30
})
watch(user, (newUser, oldUser) => {
console.log(newUser === oldUser) // 输出:true,因为oldUser是新值的引用
})解决方案:监听返回对象的函数,而不是对象本身
watch(
() => ({ ...user }), // 创建对象的副本
(newUser, oldUser) => {
console.log(newUser === oldUser) // 输出:false
}
)2. 忘记清理异步操作
// 不推荐:未清理异步操作,可能导致竞态条件
watch(searchQuery, async (newQuery) => {
const result = await fetch(`/api/search?q=${newQuery}`)
const data = await result.json()
console.log('Search result:', data)
// 问题:如果searchQuery快速变化,可能会导致多个请求同时进行,最终结果可能不是最新的
})解决方案:使用onCleanup清理异步操作
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
try {
const result = await fetch(`/api/search?q=${newQuery}`, {
signal: controller.signal
})
const data = await result.json()
console.log('Search result:', data)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search error:', error)
}
}
})3. 无限循环
// 不推荐:在侦听器中修改被监听的数据,导致无限循环
const count = ref(0)
watch(count, (newValue) => {
count.value++ // 修改被监听的数据,导致无限循环
})解决方案:避免在侦听器中修改被监听的数据,或者使用条件判断
const count = ref(0)
watch(count, (newValue) => {
if (newValue < 10) {
// 只有在特定条件下才修改数据
count.value++
}
})4. 监听对象属性时,依赖未正确跟踪
// 不推荐:直接监听对象的属性,可能导致依赖未正确跟踪
const user = reactive({
name: 'Alice'
})
watch(user.name, (newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`)
}) // 错误:user.name不是响应式引用解决方案:使用函数返回需要监听的属性
watch(
() => user.name, // 正确:使用函数返回需要监听的属性
(newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`)
}
)九、watch与watchEffect的性能比较
1. 性能开销
watch:性能开销较小,因为它只在指定的依赖变化时执行watchEffect:性能开销可能较大,因为它会自动追踪所有依赖,并在任何依赖变化时执行
2. 初始执行
watch:默认不执行,只有依赖变化时执行watchEffect:默认执行一次,依赖变化时再执行
3. 依赖追踪
watch:手动指定依赖,追踪更精确watchEffect:自动追踪所有依赖,可能追踪到不必要的依赖
4. 适用场景的性能建议
- 对于需要频繁执行的副作用,使用
watch并手动指定依赖 - 对于大型对象,避免使用
deep: true选项,而是监听特定属性 - 对于频繁变化的数据,使用防抖或节流
十、侦听器与相关API的比较
1. watch vs computed
computed:用于派生值,具有缓存机制watch:用于执行副作用,无缓存机制- 选择建议:当需要派生值时使用
computed,当需要执行副作用时使用watch
2. watch vs watchEffect
watch:需要手动指定依赖,可以访问旧值,控制更精细watchEffect:自动追踪依赖,初始执行一次,无法直接访问旧值- 选择建议:当需要访问旧值或精细控制时使用
watch,当只需执行副作用时使用watchEffect
3. watch vs methods
methods:需要手动调用,不会自动执行watch:自动执行,当依赖变化时- 选择建议:当需要自动执行副作用时使用
watch,当需要手动触发时使用methods
十一、侦听器的调试
1. 使用console.log调试
watch(count, (newValue, oldValue) => {
console.log('Count changed:', oldValue, '->', newValue)
// 可以在这里添加更多调试信息
})2. 使用debugger调试
watch(count, (newValue, oldValue) => {
debugger // 断点调试
console.log('Count changed:', oldValue, '->', newValue)
})3. 查看依赖
watchEffect((onCleanup) => {
console.log('watchEffect executing...')
console.log('Count:', count.value)
console.log('Message:', message.value)
// 可以通过输出查看当前副作用依赖了哪些数据
})十二、总结
1. 核心概念
- 侦听器用于监听响应式数据变化并执行副作用
- Vue 3提供了
watch和watchEffect两个主要的侦听器API watch需要手动指定依赖,可以访问新值和旧值watchEffect自动追踪依赖,初始执行一次,无法直接访问旧值- 侦听器可以清理副作用,避免竞态条件
2. 使用原则
- 根据需要选择合适的侦听器API
- 避免在侦听器中修改响应式数据,防止无限循环
- 一定要清理异步副作用,避免竞态条件
- 避免过度使用侦听器,简单的数据转换使用计算属性
- 注意性能,避免深度监听大型对象
3. 最佳实践
- 选择合适的侦听器API
- 清理副作用
- 避免过度使用侦听器
- 注意性能
- 使用防抖或节流处理频繁变化的数据
4. 常见错误
- 监听reactive对象时无法获取旧值
- 忘记清理异步操作
- 无限循环
- 监听对象属性时依赖未正确跟踪
十三、练习题
基础练习:
- 创建一个计数器,使用
watch监听其变化并输出旧值和新值 - 使用
watchEffect监听计数器变化并更新文档标题
- 创建一个计数器,使用
进阶练习:
- 实现一个搜索功能,使用
watch监听搜索关键词变化并调用API - 添加防抖机制,优化搜索性能
- 使用
onCleanup清理之前的搜索请求
- 实现一个搜索功能,使用
综合练习:
- 实现一个用户管理系统,使用侦听器监听用户数据变化
- 当用户数据变化时,保存到本地存储
- 当本地存储中的数据变化时,同步到应用状态
- 使用
watchEffect处理初始数据加载
性能优化练习:
- 实现一个大型对象的监听,比较直接监听、监听特定属性和深度监听的性能差异
- 为频繁变化的数据添加防抖或节流
- 比较
watch和watchEffect的性能差异
十四、扩展阅读
在下一集中,我们将学习"自定义ref实现原理",深入探讨如何在Vue 3中创建自定义的响应式引用。