一、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前端网发表,如需转载,请注明页面地址。
code前端网

