Code前端首页关于Code前端联系我们

一、Vue2 里基础的 watch 怎么用?

terry 13小时前 阅读数 12 #Vue
文章标签 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 = 19watch不会触发,这时候得开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,如果给对象新增属性(比如给useraddress),这个新属性没有被劫持,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.$setsplice
    // 错误写法:直接改索引,不触发响应式
    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里,监听$routepathparamsquery等属性,比如商品详情页,监听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.nameuser.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前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门