在 Vue 3 的 Composition API 中,侦听器是响应式编程的核心部分之一。它允许我们观察和响应响应式数据的变化,执行副作用(side effects)或异步操作。Vue 提供了两个主要的侦听器函数:watch 和 watchEffect。
侦听器(Watchers)用于观察响应式数据的变化,并在变化发生时执行特定的逻辑。这在以下场景中特别有用:
Composition API 提供了更灵活和强大的侦听器功能,相比 Options API 的 watch 选项,它在处理复杂逻辑和重用侦听器逻辑方面更加强大。
watch() 函数watch() 函数用于显式地侦听一个或多个响应式数据源,并在它们变化时执行回调函数。
import { ref, watch } from 'vue'
// 语法:watch(source, callback, options?)
const count = ref(0)
// 侦听单个数据源
watch(count, (newValue, oldValue) => {
console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})
// 触发变化
count.value++ // 输出: count 从 0 变为 1
| 参数 | 类型 | 说明 |
|---|---|---|
source |
Getter 函数 / Ref / Reactive 对象 / 数组 | 要侦听的数据源。可以是: 1. 一个返回值的 getter 函数 2. 一个 ref 3. 一个 reactive 对象 4. 包含以上类型的数组 |
callback |
函数 | 数据变化时执行的回调函数。接收新值和旧值作为参数。 |
options |
对象(可选) | 配置选项,包括:immediate、deep、flush、onTrack、onTrigger |
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 侦听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// 也可以使用 getter 函数
watch(() => count.value, (newVal, oldVal) => {
console.log(`count via getter: ${oldVal} → ${newVal}`)
})
import { reactive, watch } from 'vue'
const state = reactive({
count: 0,
user: {
name: '张三',
age: 25
}
})
// 侦听整个 reactive 对象(需要 deep: true)
watch(
() => state,
(newVal, oldVal) => {
console.log('state 变化了:', newVal)
},
{ deep: true }
)
// 侦听 reactive 对象的特定属性(推荐)
watch(
() => state.count,
(newVal, oldVal) => {
console.log(`state.count: ${oldVal} → ${newVal}`)
}
)
// 侦听嵌套属性
watch(
() => state.user.name,
(newName, oldName) => {
console.log(`用户名: ${oldName} → ${newName}`)
}
)
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const age = ref(25)
// 侦听多个 ref
watch([firstName, lastName, age], (newValues, oldValues) => {
const [newFirst, newLast, newAge] = newValues
const [oldFirst, oldLast, oldAge] = oldValues
console.log(`变化: ${oldFirst}${oldLast}(${oldAge}) → ${newFirst}${newLast}(${newAge})`)
})
// 侦听混合数据源
watch(
[() => firstName.value, () => age.value],
([newFirst, newAge], [oldFirst, oldAge]) => {
console.log(`firstName: ${oldFirst} → ${newFirst}, age: ${oldAge} → ${newAge}`)
}
)
import { ref, watch } from 'vue'
const count = ref(0)
// 设置 immediate: true,侦听器会立即执行一次
watch(
count,
(newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
},
{ immediate: true }
)
// 立即输出: count: undefined → 0
count.value = 1 // 输出: count: 0 → 1
import { reactive, watch } from 'vue'
const state = reactive({
user: {
name: '张三',
details: {
age: 25,
email: 'zhangsan@example.com'
}
}
})
// 深度侦听整个对象
watch(
() => state.user,
(newVal, oldVal) => {
console.log('user 对象发生变化:', newVal)
},
{ deep: true }
)
// 修改嵌套属性会触发侦听器
state.user.details.age = 26 // 会触发侦听器
state.user.name = '李四' // 会触发侦听器
深度侦听(deep: true)会遍历对象的所有嵌套属性,这可能会在大型对象上产生性能开销。只在实际需要时使用深度侦听。
import { ref, watch } from 'vue'
const count = ref(0)
// flush: 'pre' - 默认值,在组件更新前执行
watch(
count,
() => {
console.log('pre: 在组件更新前执行')
},
{ flush: 'pre' }
)
// flush: 'post' - 在组件更新后执行
watch(
count,
() => {
console.log('post: 在组件更新后执行')
},
{ flush: 'post' }
)
// flush: 'sync' - 同步执行,在依赖变化时立即执行
watch(
count,
() => {
console.log('sync: 立即执行')
},
{ flush: 'sync' }
)
watchEffect() 函数watchEffect() 函数会自动追踪其回调函数中使用的响应式依赖,并在这些依赖变化时重新执行回调。它不需要显式指定要侦听的数据源。
watchEffect 会自动追踪回调函数中访问的所有响应式属性,无需手动指定要侦听的数据源。
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// watchEffect 会自动追踪 count 和 message
watchEffect(() => {
console.log(`count: ${count.value}, message: ${message.value}`)
// 这里访问了 count.value 和 message.value,
// 所以当它们中任何一个变化时,都会重新执行这个函数
})
// 触发重新执行
count.value++ // 输出: count: 1, message: Hello
message.value = 'Hi' // 输出: count: 1, message: Hi
watch vs watchEffect| 特性 | watch |
watchEffect |
|---|---|---|
| 数据源 | 需要显式指定要侦听的数据源 | 自动追踪回调函数中访问的响应式依赖 |
| 初始执行 | 默认不立即执行(可通过 immediate: true 启用) |
立即执行一次 |
| 新旧值 | 可以访问旧值和新值 | 不能访问旧值,只能访问当前值 |
| 使用场景 | 需要精确控制侦听的数据源时 需要访问旧值时 |
自动依赖追踪更方便时 不需要旧值时 |
| 性能 | 更精确控制,可能更高效 | 自动依赖追踪可能包含不需要的依赖 |
import { ref, watch } from 'vue'
// 搜索关键词
const searchQuery = ref('')
// 搜索结果
const searchResults = ref([])
// 加载状态
const isLoading = ref(false)
// 防抖计时器
let debounceTimer = null
// 侦听搜索关键词的变化
watch(searchQuery, (newQuery) => {
// 清除之前的计时器
clearTimeout(debounceTimer)
// 如果搜索词为空,清空结果
if (!newQuery.trim()) {
searchResults.value = []
return
}
// 设置防抖计时器
debounceTimer = setTimeout(async () => {
isLoading.value = true
try {
// 模拟 API 调用
const response = await fetch(
`https://api.example.com/search?q=${encodeURIComponent(newQuery)}`
)
const data = await response.json()
searchResults.value = data.results
} catch (error) {
console.error('搜索失败:', error)
searchResults.value = []
} finally {
isLoading.value = false
}
}, 500) // 500ms 防抖延迟
})
// 当用户输入时自动触发搜索
// searchQuery.value = 'Vue.js' // 会触发防抖搜索
import { reactive, watchEffect } from 'vue'
// 表单数据
const formData = reactive({
title: '',
content: '',
tags: [],
lastSaved: null
})
// 自动保存的防抖计时器
let saveTimer = null
// 使用 watchEffect 自动追踪表单数据变化
watchEffect((onCleanup) => {
// 访问表单数据,自动追踪依赖
const { title, content, tags } = formData
// 清理之前的计时器
onCleanup(() => {
clearTimeout(saveTimer)
})
// 设置防抖保存
saveTimer = setTimeout(async () => {
try {
// 模拟保存到服务器
await fetch('/api/save-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content, tags })
})
// 更新最后保存时间
formData.lastSaved = new Date().toLocaleTimeString()
console.log('表单已自动保存')
} catch (error) {
console.error('保存失败:', error)
}
}, 1000) // 1秒防抖
})
// 当用户修改表单时,会自动触发保存
// formData.title = '新标题' // 会触发自动保存
import { watch, ref } from 'vue'
import { useRoute } from 'vue-router'
// 获取当前路由
const route = useRoute()
const userId = ref(null)
const userData = ref(null)
// 监听路由参数变化
watch(
() => route.params.id,
async (newId) => {
if (!newId) return
userId.value = newId
try {
// 根据新的用户ID加载用户数据
const response = await fetch(`/api/users/${newId}`)
userData.value = await response.json()
} catch (error) {
console.error('加载用户数据失败:', error)
userData.value = null
}
},
{ immediate: true } // 组件创建时立即执行一次
)
// 监听查询参数变化
watch(
() => route.query.page,
(newPage) => {
console.log('页码变化:', newPage)
// 重新加载列表数据
loadData(newPage || 1)
}
)
watch 和 watchEffect 都会返回一个停止函数,调用这个函数可以停止侦听。
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// watch 返回停止函数
const stopWatch = watch(count, (newVal) => {
console.log('watch:', newVal)
if (newVal >= 5) {
console.log('达到5,停止watch')
stopWatch() // 停止侦听
}
})
// watchEffect 也返回停止函数
const stopEffect = watchEffect(() => {
console.log('watchEffect:', count.value)
if (count.value >= 3) {
console.log('达到3,停止watchEffect')
stopEffect() // 停止侦听
}
})
// 在组件卸载时停止所有侦听器
import { onUnmounted } from 'vue'
onUnmounted(() => {
stopWatch()
stopEffect()
})
watchEffect 的回调函数接收一个 onCleanup 参数,用于注册清理函数。当侦听器重新执行或停止时,清理函数会被调用。
import { ref, watchEffect } from 'vue'
const searchQuery = ref('')
watchEffect((onCleanup) => {
// 创建 AbortController 用于取消请求
const controller = new AbortController()
const signal = controller.signal
// 注册清理函数
onCleanup(() => {
console.log('清理之前的请求')
controller.abort() // 取消请求
})
// 执行异步操作
if (searchQuery.value.trim()) {
fetch(`/api/search?q=${searchQuery.value}`, { signal })
.then(response => response.json())
.then(data => {
console.log('搜索结果:', data)
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('搜索失败:', err)
}
})
}
})
// 当 searchQuery 快速变化时,
// 之前的请求会被自动取消,避免竞态条件
import { ref, watch } from 'vue'
const userId = ref(1)
// 异步侦听器
watch(
userId,
async (newId, oldId, onCleanup) => {
let cancelled = false
// 注册清理函数
onCleanup(() => {
cancelled = true
})
try {
// 模拟异步数据获取
const response = await fetch(`/api/user/${newId}`)
const userData = await response.json()
// 检查是否被取消
if (!cancelled) {
console.log('用户数据:', userData)
// 更新组件状态...
}
} catch (error) {
if (!cancelled) {
console.error('加载用户数据失败:', error)
}
}
},
{ immediate: true }
)
import { ref, watch } from 'vue'
const count = ref(0)
const double = ref(0)
watch(
count,
(newVal) => {
double.value = newVal * 2
},
{
onTrack(e) {
console.log('依赖被追踪:', e)
},
onTrigger(e) {
console.log('依赖变化触发:', e)
}
}
)
// 修改 count 会触发 onTrack 和 onTrigger
count.value = 5
deep: true,因为它有性能开销onCleanup 清理异步操作,避免内存泄漏watch 或 watchEffect尝试修改以下值,观察控制台输出(打开浏览器开发者工具查看)
Vue 3 Composition API 中的侦听器提供了强大而灵活的方式来响应数据变化。通过 watch 和 watchEffect,我们可以:
理解 watch 和 watchEffect 的区别及适用场景,可以帮助我们编写更高效、更易维护的 Vue 应用。记住始终遵循最佳实践,避免常见陷阱。
watch 和 watchEffect 的主要区别是什么?deep: true 选项?watchEffect 中使用 onCleanup?