一、Vue2 里基础的 watch 怎么用?
在Vue2开发里,数据变化后要执行逻辑是常有的事儿——比如用户切换城市后请求新数据、输入框内容变化时实时校验、购物车数量变了要更新总价…这时候「监听」就成了关键技能,但Vue2里监听数据有好几种玩法,watch、computed还有一堆细节坑,新手很容易搞混或者踩雷,今天就用问答形式,把Vue2监听的门道掰开了讲,从基础到进阶一次搞懂~
先从最常用的`watch`说起,它是Vue实例上的一个选项,专门用来监听**响应式数据**的变化(比如data里的属性、computed的值),基本用法分两种:监听简单数据,和监听对象/数组。监听简单数据(如字符串、数字)
假设页面有个下拉框切换城市,选完要请求该城市的商家数据,代码可以这么写:
export default { data() { return { city: '北京' // 默认城市 } }, watch: { // 监听city属性 city(newVal, oldVal) { console.log(`从${oldVal}切换到${newVal}`); this.fetchShops(newVal); // 调用接口拿新城市的商家数据 } }, methods: { fetchShops(city) { // 调接口逻辑... } } }
这里newVal
是变化后的值,oldVal
是变化前的值,数据变了就执行handler里的逻辑。
监听对象/数组,要开「深度监听」
如果监听的是对象(比如用户信息user: { name: '小明', age: 18 }
)或数组,直接写会有坑——因为Vue默认只监听引用变化,不关心内部属性,比如直接改user.age = 19
,watch
不会触发,这时候得开deep: true
:
watch: { user: { handler(newVal) { console.log('用户信息变了:', newVal); }, deep: true // 开启深度监听,遍历对象内部所有属性 } }
但注意:deep
会递归遍历对象的所有属性,性能消耗大,如果只关心对象某一个属性(比如user.age
),更高效的做法是监听具体属性:
watch: { 'user.age'(newAge) { console.log('年龄变了:', newAge); } }
页面加载时就执行一次监听逻辑
有时候需要页面刚加载,就执行一次handler(比如默认城市加载后立即请求数据),这时候加immediate: true
:
watch: { city: { handler(newVal) { this.fetchShops(newVal); }, immediate: true // 页面初始化时就执行handler } }
watch 和 computed 该怎么选?
很多新手分不清这俩,其实核心差异是「为什么监听」和「怎么用」:
computed:依赖变化时「自动计算新值」
适合多个响应式数据拼出一个结果的场景,比如购物车总价(商品数量×单价 + 运费),特点是:
- 有缓存:依赖的数据不变,多次访问
computed
属性会直接拿缓存,不重复计算; - 惰性执行:只有当
computed
属性被页面使用时,才会计算(比如模板里用了{{totalPrice}}
)。
举个栗子:
computed: { totalPrice() { // 依赖goodsList和freight,任意一个变化,totalPrice自动更新 return this.goodsList.reduce((sum, item) => sum + item.price * item.quantity, 0) + this.freight; } }
watch:数据变化后「执行复杂逻辑」
适合数据变化后要做异步操作、复杂逻辑、旧值新值对比的场景。
- 用户修改表单后,标记“未保存”状态;
- 路由参数变化后,请求新数据; 变化时,防抖调搜索接口。
特点是:
- 主动触发:数据一变就执行,不管有没有被页面使用;
- 能拿到旧值:
handler(newVal, oldVal)
里可以对比变化前后的值; - 支持异步:handler里可以写
setTimeout
、调接口这些异步逻辑(computed里不能写异步,因为要返回值)。
举个栗子:
watch: { userForm: { handler() { this.isSaved = false; // 表单变化后,标记为“未保存” }, deep: true // 因为userForm是对象,要监听内部变化 } }
一句话总结区别
- 想“自动算新值”→ 用
computed
; - 想“数据变了执行逻辑”→ 用
watch
。
对象和数组监听,这些坑要避开!
Vue2的响应式基于Object.defineProperty
实现,但对象和数组的监听有特殊逻辑,稍不注意就“监了但没完全监”。
对象监听:新增属性不触发响应式
Vue对对象的响应式处理,是给已有属性加getter/setter
,如果给对象新增属性(比如给user
加address
),这个新属性没有被劫持,watch
也不会触发。
解决方法分两种:
- 场景1:新增根属性(如
user.address
)→ 用this.$set
// 错误写法:直接赋值,不触发响应式 this.user.address = '朝阳区';
// 正确写法:用$set给对象新增属性 this.$set(this.user, 'address', '朝阳区');
- 场景2:修改**已有属性的深层值**(如`user.info.age`,`info`原本就存在)→ 开`deep: true`
```js
watch: {
user: {
handler(newVal) {
console.log('年龄变了:', newVal.info.age);
},
deep: true // 遍历user所有属性,监听内部变化
}
}
数组监听:这些操作不触发响应式
Vue对数组的7个“变异方法”(push/pop/shift/unshift/splice/sort/reverse
)做了重写,调用这些方法会触发响应式,但如果是直接改索引(如this.list[0] = 100
)或改length(如this.list.length = 0
),watch
不会触发。
解决方法:
- 改索引→ 用
this.$set
或splice
// 错误写法:直接改索引,不触发响应式 this.list[0] = 100;
// 正确写法1:用$set this.$set(this.list, 0, 100);
// 正确写法2:用splice this.list.splice(0, 1, 100);
- 改length→ 用`splice`
```js
// 错误写法:直接改length,不触发响应式
this.list.length = 0;
// 正确写法:用splice清空数组
this.list.splice(0);
路由变化怎么监听?
单页面应用里,路由变化很常见(比如商品详情页,不同id
对应不同数据),监听路由有两种主流方式:
watch $route 对象
在组件的watch
里,监听$route
的path
、params
、query
等属性,比如商品详情页,监听params.id
变化:
watch: { '$route.params.id'(newId, oldId) { console.log(`从商品${oldId}跳到${newId}`); this.fetchGoodsDetail(newId); // 请求新商品数据 } }
导航守卫 beforeRouteUpdate
如果是同一个组件被复用时(比如从/goods/1
跳到/goods/2
,组件实例复用),可以用组件内的导航守卫beforeRouteUpdate
,在路由更新前执行逻辑:
beforeRouteUpdate(to, from, next) { // to是目标路由,from是当前路由 this.fetchGoodsDetail(to.params.id); next(); // 必须调用next()放行路由 }
两种方式怎么选?
- 简单场景(只需要参数变化后执行逻辑)→ 用
watch $route
; - 复杂场景(路由更新前要做权限判断、数据预加载)→ 用
beforeRouteUpdate
。
监听的进阶玩法:动态监听与销毁
实际开发中,经常需要“按需监听”(比如页面加载后监听,离开前销毁),或者“监听多个数据源”,这时候得用更灵活的写法。
动态添加/销毁监听:this.$watch
watch
选项是“静态”的(初始化时就绑定),而this.$watch
可以在生命周期钩子里动态添加监听,离开时销毁(防止内存泄漏)。
举个栗子:搜索框输入时防抖调接口,页面销毁前取消监听:
export default { data() { return { searchKey: '' } }, mounted() { // 动态添加监听,返回销毁函数 this.searchWatcher = this.$watch('searchKey', (newVal) => { this.debounceFetch(newVal); // 防抖调接口 }, { immediate: true // 初始化时执行一次 }); }, beforeDestroy() { // 销毁监听 this.searchWatcher(); }, methods: { debounceFetch: _.debounce(function(val) { // 调搜索接口逻辑... }, 300) } }
监听多个数据源:数组或函数
如果想同时监听多个数据的变化(比如user.name
和user.age
),可以把watch
的key写成数组:
watch: { ['user.name', 'user.age'](newVals, oldVals) { console.log('姓名或年龄变了:', newVals, oldVals); } }
或者更灵活的,用函数返回一个组合值,监听这个组合值的变化(相当于监听多个数据):
computed: { userInfo() { return { name: this.user.name, age: this.user.age }; } }, watch: { userInfo(newVal) { console.log('用户信息有变化:', newVal); } }
原理补充:为什么Vue2监听有这些“特殊规则”?
理解底层原理,能更通透地避坑,Vue2的响应式基于Object.defineProperty
,它对对象和数组的处理逻辑不同:
-
对象:通过遍历已有属性,给每个属性加
getter/setter
,所以新增属性没被劫持,响应式失效;修改已有属性会触发setter
,通知watch
更新。 -
数组:因为数组索引太多(比如长度10000),给每个索引加
getter/setter
性能太差,所以Vue只重写了7个“变异方法”,调用这些方法时手动触发更新;而直接改索引/length不会触发setter
,自然监听不到。
Vue2里监听数据要根据场景选工具:简单数据变化用基础watch,对象数组注意深监听和响应式坑,多数据计算用computed,路由变化选watch或导航守卫,进阶场景用$watch动态管理…把这些门道吃透,写代码时就不会再对着“数据变了但逻辑没执行”抓头发啦~如果还有疑问,评论区随时唠~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。