Vue 2 与 Vue 3 侦听器完全指南:从 Options API 到组合式 API 的演进
前言
在之前的博客中,我们聊了 Vue 的指令、响应式 API(ref/reactive),以及从 Options API 到 Composition API 的演进。今天,我们把目光投向 Vue 中另一个核心概念——侦听器(watch)。
watch 是 Vue 中用于观察和响应数据变化的机制。当需要在数据变化时执行异步操作或开销较大的操作时,watch 是最有力的工具。无论是搜索框的实时搜索、表单联动的级联更新,还是根据路由变化重新加载数据,都离不开 watch。
本文将系统梳理 Vue 2 和 Vue 3 中 watch 的用法差异、底层原理、实战案例和最佳实践,帮你彻底掌握这个 Vue 开发中的核心工具。
一、为什么需要 watch?
在 Vue 中,数据变化驱动视图更新是核心机制。但有些场景下,我们需要在数据变化时执行自定义逻辑,而不是仅仅更新视图:
- 搜索框实时搜索:用户输入关键词变化时,自动发起 API 请求获取搜索结果
- 表单联动:选择省份时,自动加载该省份的城市列表
- 深度监听复杂对象:监听表单对象的某个字段变化后自动保存
- 路由参数变化:监听
$route变化,重新加载页面数据 - 数据持久化:监听数据变化,自动保存到 localStorage
watchvscomputed的选择:计算属性(computed)用于派生数据,具有缓存特性;侦听器(watch)用于执行副作用,适合异步请求、DOM 操作等场景。两者各有分工,不可互相替代。
二、Vue 2 中的 watch(Options API)
2.1 基本用法
在 Vue 2 中,watch 是一个选项对象,键是要观察的属性名(支持点号分隔的路径),值是对应的回调函数或包含选项的对象。
函数式写法(适用于简单监听):
export default {
data() {
return {
searchKeyword: ''
}
},
watch: {
// 当 searchKeyword 发生变化时,这个函数将被调用
searchKeyword(newVal, oldVal) {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
// 执行搜索逻辑
this.fetchSearchResults(newVal)
}
},
methods: {
fetchSearchResults(keyword) {
// 发起 API 请求
}
}
}
对象式写法(适用于复杂监听,支持更多配置):
export default {
data() {
return {
user: {
name: '张三',
profile: {
age: 25,
city: '深圳'
}
}
}
},
watch: {
user: {
handler(newVal, oldVal) {
console.log('用户信息变化:', newVal)
this.syncUserData(newVal)
},
deep: true, // 深度监听对象内部变化
immediate: true // 立即执行,组件初始化时触发一次
}
}
}
2.2 监听对象的特定属性
使用点号分隔的字符串作为键名,可以监听对象中某个具体属性的变化:
export default {
data() {
return {
user: {
name: '张三',
age: 25
}
}
},
watch: {
// 只监听 user 对象的 name 属性
'user.name': function(newVal, oldVal) {
console.log(`姓名从 "${oldVal}" 变为 "${newVal}"`)
}
}
}
2.3 选项详解:deep 和 immediate
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
deep |
boolean | false |
是否深度监听对象内部属性的变化 |
immediate |
boolean | false |
是否在组件初始化时立即执行回调 |
deep: true 的行为:
watch: {
userInfo: {
handler(newVal) {
console.log('userInfo 变化了:', newVal)
},
deep: true // 不设置 deep,修改 userInfo.name 不会触发回调
}
}
当 deep: true 时,userInfo.name 或 userInfo.age 的任何变化都会触发 handler 函数。
2.4 $watch API(程序式监听)
Vue 2 提供了 $watch 实例方法,可以在任何地方动态创建监听器:
export default {
mounted() {
// 创建监听器
const unwatch = this.$watch(
'count',
(newVal, oldVal) => {
console.log(`count 变化: ${oldVal} -> ${newVal}`)
},
{ immediate: true, deep: false }
)
// 在适当的时机取消监听
// unwatch()
},
beforeDestroy() {
// 组件销毁前取消监听,防止内存泄漏
if (this.unwatch) {
this.unwatch()
}
}
}
⚠️ 注意:
$watch返回一个取消监听函数,在组件销毁前应主动调用,避免内存泄漏。
三、Vue 3 中的 watch(组合式 API)
Vue 3 的 watch 发生了重大变革——从选项对象变成了独立的函数,可以导入并在 setup() 中使用。
3.1 基本用法
Vue 3 的 watch 函数接收三个参数:
watch(source, callback, options)
| 参数 | 类型 | 说明 |
|---|---|---|
source |
ref / reactive / getter / 数组 | 要监听的数据源 |
callback |
Function | 数据变化时执行的回调函数 |
options |
Object | 可选配置项(deep、immediate、flush 等) |
3.2 监听不同类型的数据源
监听 ref:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const message = ref('Hello')
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 变化: ${oldVal} -> ${newVal}`)
})
// 监听 ref 时,直接传入 ref 变量即可,不需要 .value
watch(message, (newVal, oldVal) => {
console.log(`消息变化: ${oldVal} -> ${newVal}`)
})
</script>
监听 reactive 对象:
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: '张三',
age: 25,
address: {
city: '深圳',
district: '南山区'
}
})
// 直接监听 reactive 对象——默认开启 deep(Vue 3 的特性)
watch(user, (newVal, oldVal) => {
console.log('user 变化了:', newVal)
})
// 监听 reactive 对象的某个具体属性——需要使用 getter 函数
watch(
() => user.name,
(newVal, oldVal) => {
console.log(`姓名变化: ${oldVal} -> ${newVal}`)
}
)
</script>
⚠️ 重要:监听
reactive对象时,默认启用深度监听(与 Vue 2 不同)。但oldValue和newValue会指向同一个对象,无法获取变化前的完整快照。
监听 getter 函数:
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
// 使用 getter 函数返回需要监听的值
watch(
() => count.value + user.name.length,
(newVal, oldVal) => {
console.log(`总和变化: ${oldVal} -> ${newVal}`)
}
)
</script>
3.3 监听多个数据源
Vue 3 支持在一个 watch 中同时监听多个数据源:
<script setup>
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三', age: 25 })
// 监听多个源——传入数组
watch(
[count, () => user.name, () => user.age],
([newCount, newName, newAge], [oldCount, oldName, oldAge]) => {
console.log(`count: ${oldCount}->${newCount}`)
console.log(`姓名: ${oldName}->${newName}`)
console.log(`年龄: ${oldAge}->${newAge}`)
}
)
</script>
3.4 选项详解(Vue 3 新增)
Vue 3 的 watch 在 Vue 2 的 deep 和 immediate 基础上,新增了 flush 选项:
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
deep |
boolean | false(ref)/ true(reactive) |
深度监听 |
immediate |
boolean | false |
立即执行回调 |
flush |
'pre' / 'post' / 'sync' |
'pre' |
控制回调执行时机 |
onTrack |
Function | — | 调试:依赖被追踪时触发 |
onTrigger |
Function | — | 调试:依赖变化触发更新时触发 |
flush 选项详解:
// DOM 更新前执行(默认)
watch(source, callback, { flush: 'pre' })
// DOM 更新后执行——适合需要操作 DOM 的场景
watch(source, callback, { flush: 'post' })
// 同步执行——数据变化时立即执行,性能开销较大
watch(source, callback, { flush: 'sync' })
3.5 停止监听
watch 函数会返回一个停止监听的函数:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// watch 返回停止函数
const stopWatch = watch(count, (newVal) => {
console.log('count:', newVal)
})
// 在适当的时机停止监听
const handleStop = () => {
stopWatch()
}
// 组件卸载时会自动停止,一般不需要手动调用
</script>
3.6 watchEffect:自动追踪依赖
Vue 3 还提供了 watchEffect,与 watch 不同的是,它会自动收集依赖,并在任何依赖变化时重新执行:
<script setup>
import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const user = reactive({ name: '张三' })
// watchEffect 自动追踪内部用到的所有响应式数据
watchEffect(() => {
console.log(`count 是 ${count.value},用户名是 ${user.name}`)
// 只要 count 或 user.name 变化,这个函数就会重新执行
})
// watchEffect 也返回停止函数
const stop = watchEffect(() => {
// 副作用逻辑
})
// 停止监听
// stop()
</script>
watch vs watchEffect 对比:
| 对比维度 | watch |
watchEffect |
|---|---|---|
| 依赖声明 | 显式指定数据源 | 自动收集依赖 |
| 执行时机 | 惰性:依赖变化时才执行 | 立即执行,首次加载就执行 |
| 获取旧值 | ✅ 可获取 (newVal, oldVal) |
❌ 只能获取当前值 |
| 适用场景 | 需要精确控制监听源和旧值 | 多个依赖联动,不需要旧值 |
四、Vue 2 与 Vue 3 选项式 API 的 watch 对比
除了组合式 API 的 watch 函数,Vue 3 仍然支持选项式 API 中的 watch 选项:
Vue 2:
export default {
data() {
return { count: 0 }
},
watch: {
count(newVal, oldVal) {
console.log(`count: ${oldVal} -> ${newVal}`)
}
}
}
Vue 3(选项式 API) :
import { watch } from 'vue'
export default {
data() {
return { count: 0 }
},
watch: {
count(newVal, oldVal) {
console.log(`count: ${oldVal} -> ${newVal}`)
}
},
created() {
// 也可以在 created 中使用 watch 函数
watch(
() => this.count,
(newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
}
)
}
}
五、底层原理对比
5.1 Vue 2 的原理:基于 Object.defineProperty
Vue 2 的响应式系统基于 Object.defineProperty:
- 初始化时拦截属性:递归遍历数据对象的每一个属性,使用
Object.defineProperty拦截属性的 getter/setter - 为每个监听表达式创建 Watcher 实例
- 依赖收集:访问属性时,将当前的 Watcher 记录为该属性的依赖
- 触发更新:属性值变化时,通知相关的 Watcher,调用回调函数
Vue 2 的 watch 本质上是对 Watcher 的一种封装。
5.2 Vue 3 的原理:基于 Proxy
Vue 3 的响应式系统基于 Proxy,watch 基于 effect 副作用机制实现:
- 使用 Proxy 拦截:直接拦截对数据对象的访问和修改,无需递归遍历每个属性
- 依赖收集与触发:与 Vue 2 类似,但 Proxy 可以拦截更多操作(新增属性、删除属性、数组索引修改等)
watch内部封装effect:watch在内部封装了effect副作用函数,并配有调度器(scheduler)- 调度执行:当数据被读取时进行依赖追踪(track);当数据被修改时,通过调度器执行关联的回调函数
5.3 核心差异总结
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 底层实现 | Object.defineProperty |
Proxy |
| Watcher 机制 | 每个监听创建一个 Watcher 实例 | 基于 effect 副作用系统 |
| 新增/删除属性 | 无法自动检测 | ✅ 自动检测 |
| 数组索引修改 | 无法自动检测 | ✅ 自动检测 |
| 性能 | 初始化遍历所有属性 | 惰性代理,更高效 |
| 深度监听默认行为 | deep: false |
ref:false / reactive:true |
六、真实项目实战
6.1 场景一:搜索框实时搜索(防抖)
这是 watch 最经典的用法——监听搜索关键词变化,发起 API 请求。
Vue 2 实现:
<template>
<div>
<input v-model="searchKeyword" placeholder="请输入搜索关键词" />
<div v-if="loading">加载中...</div>
<ul>
<li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script>
import { debounce } from 'lodash-es'
export default {
data() {
return {
searchKeyword: '',
searchResults: [],
loading: false
}
},
watch: {
searchKeyword: {
handler(newVal) {
if (newVal.trim()) {
this.debouncedSearch(newVal)
} else {
this.searchResults = []
}
},
immediate: false
}
},
created() {
// 创建防抖函数
this.debouncedSearch = debounce(this.fetchSearchResults, 500)
},
methods: {
async fetchSearchResults(keyword) {
this.loading = true
try {
const res = await api.search(keyword)
this.searchResults = res.data
} finally {
this.loading = false
}
}
},
beforeDestroy() {
// 取消防抖,防止内存泄漏
this.debouncedSearch.cancel()
}
}
</script>
Vue 3 实现(组合式 API) :
<template>
<div>
<input v-model="searchKeyword" placeholder="请输入搜索关键词" />
<div v-if="loading">加载中...</div>
<ul>
<li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const searchKeyword = ref('')
const searchResults = ref([])
const loading = ref(false)
// 封装搜索逻辑
const fetchSearchResults = async (keyword) => {
if (!keyword.trim()) {
searchResults.value = []
return
}
loading.value = true
try {
const res = await api.search(keyword)
searchResults.value = res.data
} finally {
loading.value = false
}
}
// 创建防抖函数
const debouncedSearch = debounce(fetchSearchResults, 500)
// 监听搜索关键词变化
watch(searchKeyword, (newVal) => {
debouncedSearch(newVal)
})
// 组件卸载时取消防抖
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
debouncedSearch.cancel()
})
</script>
6.2 场景二:表单联动——省市区级联
监听省份变化,自动加载对应的城市列表。
Vue 3 实现:
<template>
<div>
<select v-model="selectedProvince">
<option v-for="p in provinces" :key="p.id" :value="p.id">
{{ p.name }}
</option>
</select>
<select v-model="selectedCity">
<option v-for="c in cities" :key="c.id" :value="c.id">
{{ c.name }}
</option>
</select>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
const selectedProvince = ref('')
const selectedCity = ref('')
const cities = ref([])
const provinces = reactive([
{ id: 'gd', name: '广东省' },
{ id: 'zj', name: '浙江省' },
{ id: 'js', name: '江苏省' }
])
// 模拟城市数据
const cityMap = {
'gd': ['深圳市', '广州市', '东莞市'],
'zj': ['杭州市', '宁波市', '温州市'],
'js': ['南京市', '苏州市', '无锡市']
}
// 监听省份变化,加载城市列表
watch(selectedProvince, async (newProvince) => {
if (!newProvince) {
cities.value = []
selectedCity.value = ''
return
}
// 模拟异步加载
const cityList = cityMap[newProvince] || []
cities.value = cityList.map(name => ({ id: name, name }))
selectedCity.value = ''
})
// 监听城市变化,执行后续逻辑(如加载区县)
watch(selectedCity, (newCity) => {
if (newCity) {
console.log('选中的城市:', newCity)
// 加载区县列表或执行其他逻辑
}
})
</script>
6.3 场景三:深度监听表单自动保存
监听复杂表单对象的任何变化,自动保存到 localStorage。
<script setup>
import { reactive, watch, onMounted } from 'vue'
const form = reactive({
title: '',
content: '',
tags: [],
settings: {
isPublic: true,
allowComments: true
}
})
// 从 localStorage 恢复数据
onMounted(() => {
const saved = localStorage.getItem('draft')
if (saved) {
Object.assign(form, JSON.parse(saved))
}
})
// 深度监听表单变化,自动保存草稿
watch(
form,
(newVal) => {
localStorage.setItem('draft', JSON.stringify(newVal))
console.log('草稿已自动保存')
},
{ deep: true } // 深度监听所有嵌套属性
)
</script>
6.4 场景四:监听路由变化(Vue Router)
在 Vue 3 + Vue Router 中,监听路由参数变化以重新加载页面数据。
<script setup>
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 监听路由参数变化
watch(
() => route.params.id,
(newId, oldId) => {
if (newId !== oldId) {
console.log(`路由 ID 从 ${oldId} 变为 ${newId}`)
fetchData(newId)
}
},
{ immediate: true } // 首次加载也执行
)
const fetchData = async (id) => {
// 根据 ID 加载数据
}
</script>
七、注意事项与最佳实践
7.1 ⚠️ 监听 reactive 对象时 oldValue 的问题
在 Vue 3 中,监听 reactive 对象时,oldValue 和 newValue 指向同一个对象,无法获取变化前的快照:
const user = reactive({ name: '张三', age: 25 })
// ❌ oldValue 和 newValue 是同一个对象
watch(user, (newVal, oldVal) => {
console.log(newVal === oldVal) // true
// oldVal 已经被修改,无法获取变化前的值
})
// ✅ 解决方案:使用 getter 监听具体属性
watch(
() => ({ ...user }), // 创建副本
(newVal, oldVal) => {
// 现在 newVal 和 oldVal 是不同的对象
}
)
7.2 ⚠️ watch 函数应在 setup 中同步注册
在 setup() 函数中,watch 应该同步注册:
// ❌ 错误:在异步操作后注册 watch
async setup() {
const data = await fetchData()
watch(data, () => {}) // 可能不会按预期工作
}
// ✅ 正确:同步注册
setup() {
const data = ref(null)
watch(data, () => {})
fetchData().then(res => data.value = res)
}
7.3 ⚠️ 避免在 watch 中无限循环
在 watch 回调中修改被监听的数据,可能导致无限循环:
// ❌ 危险:无限循环
const count = ref(0)
watch(count, (newVal) => {
count.value = newVal + 1 // 每次变化又触发新的变化
})
// ✅ 安全:使用条件判断
watch(count, (newVal) => {
if (newVal < 10) {
count.value = newVal + 1
}
})
7.4 ⚠️ 深度监听的性能开销
deep: true 会递归遍历对象的所有属性,对于大型嵌套对象可能带来性能问题。建议:
- 优先监听具体属性而非整个对象
- 如果必须深度监听,考虑使用
watchEffect自动追踪精确依赖 - 对于超大对象,考虑按需监听或使用
shallowRef/shallowReactive
7.5 ✅ 及时清理副作用
使用 watch 返回的停止函数清理监听,以及使用 onCleanup 清理异步请求:
import { watch, onBeforeUnmount } from 'vue'
const stopWatch = watch(source, (newVal, oldVal, onCleanup) => {
let cancelled = false
// 注册清理回调
onCleanup(() => {
cancelled = true
console.log('清理副作用')
})
// 发起异步请求
fetchData(newVal).then(res => {
if (!cancelled) {
// 更新数据
}
})
})
// 组件卸载时停止监听
onBeforeUnmount(() => {
stopWatch()
})
八、总结
Vue 2 vs Vue 3 watch 全景对比
| 对比维度 | Vue 2(Options API) | Vue 3(组合式 API) |
|---|---|---|
| 定义方式 | watch 选项对象 |
watch() 函数 |
| 数据源类型 | 字符串路径(如 'user.name') |
ref / reactive / getter / 数组 |
| 监听多个源 | 分别配置多个 watch | 一个 watch 监听数组 |
| 深度监听默认 | deep: false |
ref:false / reactive:true |
| 停止监听 | $watch 返回取消函数 |
watch() 返回停止函数 |
| 执行时机控制 | 仅 immediate |
新增 flush 选项(pre/post/sync) |
| 底层实现 | Object.defineProperty + Watcher |
Proxy + effect 副作用系统 |
| watchEffect | ❌ 无 | ✅ 自动追踪依赖 |
从 Vue 2 到 Vue 3,watch 的演进体现了 Vue 整体设计哲学的转变:从框架主导的 Options API,走向开发者主导的组合式 API。watch 函数让逻辑组织更加灵活,watchEffect 让依赖追踪更加智能。
在实际项目中:
- Vue 2 项目:继续使用 Options API 的
watch选项,或使用$watch进行程序式监听 - Vue 3 新项目:优先使用组合式 API 的
watch函数,配合<script setup>获得最佳开发体验 - 需要自动追踪多个依赖:使用
watchEffect,代码更简洁 - 需要精确控制监听源和获取旧值:使用
watch
理解 watch 的演进和正确用法,能让你在处理异步逻辑、数据联动、状态持久化等场景时更加得心应手。

浙公网安备 33010602011771号