Vue 2 与 Vue 3 侦听器完全指南:从 Options API 到组合式 API 的演进
前言在之前的博客中我们聊了 Vue 的指令、响应式 APIref/reactive以及从 Options API 到 Composition API 的演进。今天我们把目光投向 Vue 中另一个核心概念——侦听器watch。watch是 Vue 中用于观察和响应数据变化的机制。当需要在数据变化时执行异步操作或开销较大的操作时watch是最有力的工具。无论是搜索框的实时搜索、表单联动的级联更新还是根据路由变化重新加载数据都离不开watch。本文将系统梳理 Vue 2 和 Vue 3 中watch的用法差异、底层原理、实战案例和最佳实践帮你彻底掌握这个 Vue 开发中的核心工具。一、为什么需要 watch在 Vue 中数据变化驱动视图更新是核心机制。但有些场景下我们需要在数据变化时执行自定义逻辑而不是仅仅更新视图搜索框实时搜索用户输入关键词变化时自动发起 API 请求获取搜索结果表单联动选择省份时自动加载该省份的城市列表深度监听复杂对象监听表单对象的某个字段变化后自动保存路由参数变化监听$route变化重新加载页面数据数据持久化监听数据变化自动保存到 localStoragewatchvscomputed的选择计算属性computed用于派生数据具有缓存特性侦听器watch用于执行副作用适合异步请求、DOM 操作等场景。两者各有分工不可互相替代。二、Vue 2 中的 watchOptions API2.1 基本用法在 Vue 2 中watch是一个选项对象键是要观察的属性名支持点号分隔的路径值是对应的回调函数或包含选项的对象。函数式写法适用于简单监听exportdefault{data(){return{searchKeyword:}},watch:{// 当 searchKeyword 发生变化时这个函数将被调用searchKeyword(newVal,oldVal){console.log(搜索词从 ${oldVal} 变为 ${newVal})// 执行搜索逻辑this.fetchSearchResults(newVal)}},methods:{fetchSearchResults(keyword){// 发起 API 请求}}}对象式写法适用于复杂监听支持更多配置exportdefault{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 监听对象的特定属性使用点号分隔的字符串作为键名可以监听对象中某个具体属性的变化exportdefault{data(){return{user:{name:张三,age:25}}},watch:{// 只监听 user 对象的 name 属性user.name:function(newVal,oldVal){console.log(姓名从 ${oldVal} 变为 ${newVal})}}}2.3 选项详解deep 和 immediate选项类型默认值作用deepbooleanfalse是否深度监听对象内部属性的变化immediatebooleanfalse是否在组件初始化时立即执行回调deep: true的行为watch:{userInfo:{handler(newVal){console.log(userInfo 变化了:,newVal)},deep:true// 不设置 deep修改 userInfo.name 不会触发回调}}当deep: true时userInfo.name或userInfo.age的任何变化都会触发handler函数。2.4$watchAPI程序式监听Vue 2 提供了$watch实例方法可以在任何地方动态创建监听器exportdefault{mounted(){// 创建监听器constunwatchthis.$watch(count,(newVal,oldVal){console.log(count 变化:${oldVal}-${newVal})},{immediate:true,deep:false})// 在适当的时机取消监听// unwatch()},beforeDestroy(){// 组件销毁前取消监听防止内存泄漏if(this.unwatch){this.unwatch()}}}⚠️ 注意$watch返回一个取消监听函数在组件销毁前应主动调用避免内存泄漏。三、Vue 3 中的 watch组合式 APIVue 3 的watch发生了重大变革——从选项对象变成了独立的函数可以导入并在setup()中使用。3.1 基本用法Vue 3 的watch函数接收三个参数watch(source,callback,options)参数类型说明sourceref / reactive / getter / 数组要监听的数据源callbackFunction数据变化时执行的回调函数optionsObject可选配置项deep、immediate、flush等3.2 监听不同类型的数据源监听refscript 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 对象——默认开启 deepVue 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}) } ) /script3.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}) } ) /script3.4 选项详解Vue 3 新增Vue 3 的watch在 Vue 2 的deep和immediate基础上新增了flush选项选项类型默认值作用deepbooleanfalseref/truereactive深度监听immediatebooleanfalse立即执行回调flushpre/post/syncpre控制回调执行时机onTrackFunction—调试依赖被追踪时触发onTriggerFunction—调试依赖变化触发更新时触发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() } // 组件卸载时会自动停止一般不需要手动调用 /script3.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() /scriptwatchvswatchEffect对比对比维度watchwatchEffect依赖声明显式指定数据源自动收集依赖执行时机惰性依赖变化时才执行立即执行首次加载就执行获取旧值✅ 可获取(newVal, oldVal)❌ 只能获取当前值适用场景需要精确控制监听源和旧值多个依赖联动不需要旧值四、Vue 2 与 Vue 3 选项式 API 的 watch 对比除了组合式 API 的watch函数Vue 3仍然支持选项式 API 中的watch选项Vue 2exportdefault{data(){return{count:0}},watch:{count(newVal,oldVal){console.log(count:${oldVal}-${newVal})}}}Vue 3选项式 APIimport{watch}fromvueexportdefault{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.definePropertyVue 2 的响应式系统基于Object.defineProperty初始化时拦截属性递归遍历数据对象的每一个属性使用Object.defineProperty拦截属性的 getter/setter为每个监听表达式创建 Watcher 实例依赖收集访问属性时将当前的 Watcher 记录为该属性的依赖触发更新属性值变化时通知相关的 Watcher调用回调函数Vue 2 的watch本质上是对 Watcher 的一种封装。5.2 Vue 3 的原理基于ProxyVue 3 的响应式系统基于Proxywatch基于effect副作用机制实现使用 Proxy 拦截直接拦截对数据对象的访问和修改无需递归遍历每个属性依赖收集与触发与 Vue 2 类似但 Proxy 可以拦截更多操作新增属性、删除属性、数组索引修改等watch内部封装effectwatch在内部封装了effect副作用函数并配有调度器scheduler调度执行当数据被读取时进行依赖追踪track当数据被修改时通过调度器执行关联的回调函数5.3 核心差异总结对比维度Vue 2Vue 3底层实现Object.definePropertyProxyWatcher 机制每个监听创建一个 Watcher 实例基于effect副作用系统新增/删除属性无法自动检测✅ 自动检测数组索引修改无法自动检测✅ 自动检测性能初始化遍历所有属性惰性代理更高效深度监听默认行为deep: falsereffalse/ reactivetrue六、真实项目实战6.1 场景一搜索框实时搜索防抖这是watch最经典的用法——监听搜索关键词变化发起 API 请求。Vue 2 实现template div input v-modelsearchKeyword placeholder请输入搜索关键词 / div v-ifloading加载中.../div ul li v-foritem in searchResults :keyitem.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() } } /scriptVue 3 实现组合式 APItemplate div input v-modelsearchKeyword placeholder请输入搜索关键词 / div v-ifloading加载中.../div ul li v-foritem in searchResults :keyitem.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() }) /script6.2 场景二表单联动——省市区级联监听省份变化自动加载对应的城市列表。Vue 3 实现template div select v-modelselectedProvince option v-forp in provinces :keyp.id :valuep.id {{ p.name }} /option /select select v-modelselectedCity option v-forc in cities :keyc.id :valuec.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) // 加载区县列表或执行其他逻辑 } }) /script6.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 } // 深度监听所有嵌套属性 ) /script6.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指向同一个对象无法获取变化前的快照constuserreactive({name:张三,age:25})// ❌ oldValue 和 newValue 是同一个对象watch(user,(newVal,oldVal){console.log(newValoldVal)// true// oldVal 已经被修改无法获取变化前的值})// ✅ 解决方案使用 getter 监听具体属性watch(()({...user}),// 创建副本(newVal,oldVal){// 现在 newVal 和 oldVal 是不同的对象})7.2 ⚠️ watch 函数应在 setup 中同步注册在setup()函数中watch应该同步注册// ❌ 错误在异步操作后注册 watchasyncsetup(){constdataawaitfetchData()watch(data,(){})// 可能不会按预期工作}// ✅ 正确同步注册setup(){constdataref(null)watch(data,(){})fetchData().then(resdata.valueres)}7.3 ⚠️ 避免在 watch 中无限循环在watch回调中修改被监听的数据可能导致无限循环// ❌ 危险无限循环constcountref(0)watch(count,(newVal){count.valuenewVal1// 每次变化又触发新的变化})// ✅ 安全使用条件判断watch(count,(newVal){if(newVal10){count.valuenewVal1}})7.4 ⚠️ 深度监听的性能开销deep: true会递归遍历对象的所有属性对于大型嵌套对象可能带来性能问题。建议优先监听具体属性而非整个对象如果必须深度监听考虑使用watchEffect自动追踪精确依赖对于超大对象考虑按需监听或使用shallowRef/shallowReactive7.5 ✅ 及时清理副作用使用watch返回的停止函数清理监听以及使用onCleanup清理异步请求import{watch,onBeforeUnmount}fromvueconststopWatchwatch(source,(newVal,oldVal,onCleanup){letcancelledfalse// 注册清理回调onCleanup((){cancelledtrueconsole.log(清理副作用)})// 发起异步请求fetchData(newVal).then(res{if(!cancelled){// 更新数据}})})// 组件卸载时停止监听onBeforeUnmount((){stopWatch()})八、总结Vue 2 vs Vue 3 watch 全景对比对比维度Vue 2Options APIVue 3组合式 API定义方式watch选项对象watch()函数数据源类型字符串路径如user.nameref / reactive / getter / 数组监听多个源分别配置多个 watch一个watch监听数组深度监听默认deep: falsereffalse/ reactivetrue停止监听$watch返回取消函数watch()返回停止函数执行时机控制仅immediate新增flush选项pre/post/sync底层实现Object.defineProperty WatcherProxyeffect副作用系统watchEffect❌ 无✅ 自动追踪依赖从 Vue 2 到 Vue 3watch的演进体现了 Vue 整体设计哲学的转变从框架主导的 Options API走向开发者主导的组合式 API。watch函数让逻辑组织更加灵活watchEffect让依赖追踪更加智能。在实际项目中Vue 2 项目继续使用 Options API 的watch选项或使用$watch进行程序式监听Vue 3 新项目优先使用组合式 API 的watch函数配合script setup获得最佳开发体验需要自动追踪多个依赖使用watchEffect代码更简洁需要精确控制监听源和获取旧值使用watch理解watch的演进和正确用法能让你在处理异步逻辑、数据联动、状态持久化等场景时更加得心应手。