上一节说到了 computed计算属性对比 ,虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

Vue2 watch用法

 Vue2 中的 watch 是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。

Vue2 存在两种监听方式,分别是简单监听和复杂监听

简单监听:监听的是一个回调函数,当监听的值发生改变时,才会执行监听动作。

<template>
  <h2>当前求和值为:{{ sum }}</h2>
  <button @click="sum++">点击加1</button>
</template>

<script>

export default {
  name: "TestComponent",
  data() {
    return {
      sum:1
    }
  },
  watch:{
    sum(newValue, oldValue) {
      console.log('sum的值变化了',newValue, oldValue);
    }
  },
};
</script>

上面的是一个最简单的监听动作,只有在点击按钮 sum 的值变化之后,监听器 watch 才会触发。同时,我们还可以将这个方法放到 methods 中,通过方法名的方式在 watch 中实现监听效果

  watch:{
    sum:'sumAdd'
  },
  methods: {
    sumAdd(newValue, oldValue) {
       console.log('sum的值变化了',newValue, oldValue);
    }
  },

深度监听:监听的是一个包含选项的对象。除了包含简单监听的功能之外,还包含深度监听、初始化监听等。

首先,我们可以通过对象形式来实现简单监听的效果,还是按照上面的例子,例如:

// 其余代码一致
watch:{
  sum:{
    handler(newValue, oldValue) {
      console.log('sum的值变化了',newValue, oldValue);
    }
  }
},

通过对象形式实现深度监听 -- deep:true 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深,也就是说即使监听的是一个对象形式的数据,只要对象内部属性发生变化,都能被监听到。

watch:{
  sum:{
    handler(newValue, oldValue) {
      console.log('sum的值变化了',newValue, oldValue);
    },
    deep:true
  }
},

通过对象形式实现初始化监听 -- immediate:true 该回调将会在侦听开始之后被立即调用,也就是说在组件初始化时,就会监听一次,在数据改变之后继续监听

watch:{
  sum:{
    handler(newValue, oldValue) {
      console.log('sum的值变化了',newValue, oldValue);
    },
    immediate:true
  }
},

完整的对象监听:深度监听+初始化监听

watch:{
  sum:{
    handler(newValue, oldValue) {
      console.log('sum的值变化了',newValue, oldValue);
    },
    deep: true,
    immediate:true
  }
},

在Vue3 中使用 Vue2 的watch

 和 在 Vue3 中使用 Vue2 的computed 计算属性一样,直接使用 watch 配置项即可。

<template>
  <h2>当前求和值为:{{ sum }}</h2>
  <button @click="sum++">点击加1</button>
</template>

<script>
import { ref } from "vue";

export default {
  name: "TestComponent",
  watch: {
    sum: {
      handler(newValue, oldValue) {
        console.log("sum的值变化了", newValue, oldValue);
      },
      deep: true,
      immediate: true,
    },
  },

  setup() {
    let sum = ref(1);

    return {
      sum,
    };
  },
};
</script>

当页面第一次渲染时,监听器就执行了一次,这对应的是  -- immediate: true

点击按钮之后,页面渲染,同时监听器也会同步触发。

Vue3 中 watch的 概念与基本使用 

和 computed 一样,组合式api在使用时,需要先引入,再使用。

Vue3 中的 watch 是一个函数,接收三个参数,且返回值也是一个函数,可以用来停止监听。

  1. 第一个参数是侦听器的。这个来源可以是以下几种:
    • 一个函数,返回一个值
    • 一个 ref( 而不是 ref.value )
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组
  2. 第二个参数是回调函数,用来处理监听之后的动作
  3. 第三个参数则是监听配置项( 深度监听、初始化监听 等等配置)。

但是和 computed 不一样的是 在 setup 中定义的监听器不需要 return 返回,因为 监听是一种行为,而计算属性则是一个值。

<template>
  <h2>当前求和值为:{{ sum }}</h2>
  <button @click="sum++">点击加1</button>
</template>

<script>
//组合式api需要先引入再使用
import { ref ,watch} from "vue";

export default {
  name: "TestComponent",

  setup() {
    let sum = ref(1);
    
    // 不用接收,不用返回,因为监听是动作,计算属性、响应式数据、函数都是值
    watch(sum, (newValue, oldValue) => {
      console.log("sum的值变化了", newValue, oldValue);
    })

    return {
      sum,
    };
  },
};
</script>

如果你想要在达到某种条件后停止监听,也可以可以通过执行 watch 返回的函数进行

setup() {
    let sum = ref(1);
    
    // 通过接收返回值,可以停止监听
    let stopWatch = watch(sum, (newValue, oldValue) => {
      console.log("sum的值变化了", newValue, oldValue);
        
      if(条件达成){
        stopWatch()    // 执行停止函数 
      }
    })

    console.log(stopWatch)    // 一个函数

    return {
      sum,
    };
  },

Vue3 中 watch 的使用方式

上面说的Vue3 中 watch 的简单使用方式,其实就是监听单个 ref 定义的响应式数据。但是 Vue3 中的 watch 可以分为好几种情况:

情况一:监听 【ref】定义的单个【基础类型】数据:监视的是 【value】值的变化

<template>
  <h2>当前求和值为:{{ sum }}</h2>
  <button @click="sum++">点击加1</button>
</template>

<script>
import { ref ,watch} from "vue";

export default {
  name: "TestComponent",

  setup() {
    let sum = ref(1);

    watch(sum, (newValue, oldValue) => {
      console.log("sum的值变化了", newValue, oldValue);
    })

    return {
      sum
    };
  },
};
</script>

情况二:监听 【ref 定义的【对象类型】数据:监听的是【对象的地址值】,若想深度监听,需要手动开启{ deep:true }

<template>
  <p>姓名:{{ person.name }}</p>
  <p>年龄:{{ person.age }}</p>
  <p>薪资:{{ person.job.xz }}k</p>
  <button @click="person.name += '~'">更改name</button>
  <button @click="person.age++">更改age</button>
  <button @click="person.job.xz++">涨薪</button>
  <button @click="person = { name: '李四', age: 40, job: { title: '后端', xz: 2 } }">更改整个人</button>
</template>

<script setup>
import { watch, ref } from "vue";
let person = ref({
  name: "al",
  age: 28,
  job: {
    title: '前端',
    xz:1
  }
});

watch(person, (newValue, oldValue) => {
  console.log("person的值变化了", newValue, oldValue);
});
</script>

此时我们分别点击四个按钮,发现只有修改整个人的信息时,watch 才会触发。这说明,对于 ref 定义的对象类型数据,watch 是不会默认开启深度监听的。

手动开启深度监听,配置 { deep: true }

watch(person, (newValue, oldValue) => {
  console.log("person的值变化了", newValue, oldValue);
},{
  deep: true
});

可以发现,点击任意一个按钮之后,数据都会发生改变,包括内部嵌套数据,这说明现在已经开启了深度监听 。

但同时,我们也发现了一个问题,

  • 若修改的是ref定义的对象中的属性,newValue 和 oldValue 都是新值,因为它们是同一个对象。

  • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

情况三:监听 【reactive】定义的【对象类型】数据:默认开启深度监视

<template>
  <p>姓名:{{ person.name }}</p>
  <p>年龄:{{ person.age }}</p>
  <p>薪资:{{ person.job.xz }}k</p>
  <button @click="person.name += '~'">更改name</button>
  <button @click="person.age++">更改age</button>
  <button @click="person.job.xz++">涨薪</button>
  <button @click="changePerson">更改整个人</button>
</template>

<script setup>
import { watch, reactive } from "vue";
let person = reactive({
  name: "al",
  age: 28,
  job: {
    title: '前端',
    xz:1
  }
});

function changePerson() {
  person = { name: '李四', age: 40, job: { title: '后端', xz: 2 } }
}

watch(person, (newValue, oldValue) => {
  console.log("person的值变化了", newValue, oldValue);
});
</script>

点击四个按钮之后,我们发现,

  • 当只更改内部属性的值时,watch触发了。-- 证明了 reactive 定义的对象,默认开启深度监听
  • 当替换整个 reactive 响应式对象时,watch是不触发的。-- 通过直接赋值的方式无法监听到,这是因为 person 原来指向的是 响应式数据,而现在指向的是一个单纯的对象

针对第二点,我们可以猜想一下,如果直接赋值一个普通对象不行,那赋值一个reactive 转化的对象怎么样。

function changePerson() {
  person = reactive({ name: '李四', age: 40, job: { title: '后端', xz: 2 } })
}

经过实验发现,不论是赋值普通对象,还是赋值 响应式对象,都是无法被监听到的。这是因为初始化时,person 指向的是原始响应式对象的地址。经过赋值之后,person 指向的则是另外的地址。相当于 person 已经和原始的响应式对象断开连接了,所以修改是无效的。

那么我们没有办法一次性替换整个对象了么,那当然不是,vue允许我们通过合并的方式来一次性修改整个对象。注意,我这里说的是修改,而不是替换

function changePerson() {
  person = Object.assign(person, { name: '李四', age: 40, job: { title: '后端', xz: 2 } })
}

实验发现,经过 Object.assign 处理的对象,watch 是能监听到的,这是因为,它其实是把 person 的源对象,和后面的新对象进行了合并,对于相同属性的值进行了替换覆盖,替换之后,person 指向的还是原来的地址,所以可以被监听到。

既然 Vue3 默认开启了深度监听,那可以通过配置关闭么。答案是不可以,因为 Vue 在底层隐式的创建了深层监听,配置项无法修改此属性

watch(person, (newValue, oldValue) => {
  console.log("person的值变化了", newValue, oldValue);
}, {
  deep: false    // 关闭深层监听无效
});

同时,我们还可以看到,此时 newValue和oldValue还是一样的,这是因为这两个值其实都还是取的原对象的值,并没有说新建一个对象。

所以Vue3是不允许直接替换整个对象的,但是可以通过Object.assign() 来批量修改shuxn对象中的数据。且无法通过配置来取消深度监听

情况四: 监听 【ref 】或 【reactive】定义的【对象类型】数据中的【某个属性值为基本类型的属性

<template>
  <p>姓名:{{ person.name }}</p>
  <p>年龄:{{ person.age }}</p>
  <p>薪资:{{ person.job.xz }}k</p>
  <button @click="person.name += '~'">更改name</button>
  <button @click="person.age++">更改age</button>
  <button @click="person.job.xz++">涨薪</button>
  <button @click="changePerson">更改整个人</button>
</template>

<script setup>
import { watch, reactive } from "vue";
let person = reactive({
  name: "al",
  age: 28,
  job: {
    title: '前端',
    xz:1
  }
});

function changePerson() {
  person = Object.assign(person, { name: '李四', age: 40, job: { title: '后端', xz: 2 } })
}

watch(person, (newValue, oldValue) => {
  console.log("person的值变化了", newValue, oldValue);
});
</script>

按照上面 reactive 的例子,我们若是直接监视 person 对象,那无论改变哪个属性,或者修改整个对象,都会被监听到。但是我们现在只想监听 name 属性的变化,至于其他的属性,暂时不去监听。按照基本想法,就是直接监视 person.name 即可

watch(person.name, (newValue, oldValue) => {
  console.log("person.name的值变化了", newValue, oldValue);
});

但是测试发现,即使指明了 person.name 属性,在改变 name 属性时,也无法触发 watch监听。至于其他的属性或整个对象的改变,那肯定是监听不到的,因为监听源就没有包含他们。

控制台上也提示警告:监听源只能是一个 getter函数( 一个函数,返回一个值 ),一个ref,一个 reactive对象,或包含上述情况的数组

所以可以猜测,应该是监听源出了问题,导致 watch 无法监听到 person.name 属性的改变。

那按照警告的情况,person.name 不是一个ref,不是一个 reactive对象,更不是一个数组,那就只能是一个 getter 函数了,所以,以 getter 函数的形式,来实现对数据源的改写。

// 通过箭头函数的形式,返回 person.name 以此来实现对数据源的监听
watch(() => person.name, (newValue, oldValue) => {
  console.log("person.name的值变化了", newValue, oldValue);
});

通过 getter 函数改写的形式,实现了只对 person.name 属性的监听。

所以,监听 【ref 】或 【reactive】定义的【对象类型】数据中的【某个属性值为基本类型的属性】时,需要写成函数形式。

情况五: 监听 【ref 】或 【reactive】定义的【对象类型】数据中的【某个属性值为对象类型的属性

还是拿上面的例子来看,之前监听的是 person.name 属性,该属性值是一个基本类型的数据,同理,监听 person.age 也是一样的套路。那如果我想监视 person.job 的变化,那要咋办?

还是先按照 person.name 的套路来一次看看效果。

<template>
  <p>姓名:{{ person.name }}</p>
  <p>年龄:{{ person.age }}</p>
  <p>工作:{{ person.job.title }}</p>
  <p>薪资:{{ person.job.xz }}k</p>
  <button @click="person.name += '~'">更改name</button>
  <button @click="person.age++">更改age</button>
  <button @click="person.job.title = '后端'">换工作</button>
  <button @click="person.job.xz++">涨薪</button>
  <button @click=" person.job = {title: '后端', xz: 2} "> 统一更改job</button>
</template>

<script setup>
import { watch, reactive } from "vue";
let person = reactive({
  name: "al",
  age: 28,
  job: {
    title: "前端",
    xz: 1,
  },
});

// 还是按照监听 person.name 属性的套路来
watch(() => person.job,(newValue, oldValue) => {
    console.log("person.job变化了", newValue, oldValue);
  }
);
</script>

经过测试发现,当修改person.job 中的某一个单独属性时,无法触发 watch,但是在直接替换整个 person.job 对象时,会触发 watch

这说明,watch 没有对 person.job 进行深度监听,导致job 内部属性变化无法被察觉,那我们手动配置深度监听{deep:true}

// 还是按照监听 person.name 属性的套路来,但是手动配置{deep:true}开启深度监听 
watch(() => person.job,(newValue, oldValue) => {
    console.log("person.job变化了", newValue, oldValue);
  },{deep:true}
);

经过测试得知,手动配置{deep: true}之后,不论是job内部属性变化,还是job整个属性值的统一替换,都能被 watch 监听到,这说明配置的 {deep: true} 生效了。

到了这其实就已经可以结束了,因为完成了对person.job的深度监测。但是我突然想试试,如果我不通过 getter 函数的形式,直接监听 person.job 会是什么情况呢?

watch(person.job,(newValue, oldValue) => {
    console.log("person.job变化了", newValue, oldValue);
  }
);

经过测试得知,当改变 job 对象中的属性值时,watch 会被触发,但是在替换整个job对象时,watch不会被触发,这刚好和上面的 getter 函数的例子相反了。

解释一下:因为监视源是 job,所以对于 job 中属性的变化时可以触发 watch的(ps:既然都能监听到 job 对象内部属性值的变化了,那肯定就是已经开启了深度监视了,所以这里 deep 可以不做考虑了),但是因为我监视的是原本的 { title: "前端", xz: 1, } 这个对象。而 job 属性值替换整个之后,相当于我之前监视的源对象不见了,监视对象都不见了,那我还监视个锤子,所以不触发 watch

所以,监听 【ref 】或 【reactive】定义的【对象类型】数据中的【某个属性值为对象类型的属性】时,可以直接监视该对象,也可以通过函数形式来监视该对象,不过还是推荐使用函数方式

情况六:监听上述的【多个】数据

上面的所有例子,要么监视的是单个基础类型数据,要么监视的是单个对象,那如果我现在需要同时监听多个数据需要怎么办?

就拿上面的例子,我想在监视 name 属性的同时,还监听 job 对象,那又该怎么写?直接以逗号隔开么?

watch(person.name,person.job,(newValue, oldValue) => {
    console.log("person.job变化了", newValue, oldValue);
  },{deep: true}
);

 

 这么写控制台报警告了,所以我们还是以 getter函数 的形式来指定监视源

watch(()=>person.name,()=>person.job,(newValue, oldValue) => {
    console.log("person变化了", newValue, oldValue);
  },{deep: true}
);

测试后发现,以逗号隔开的 getter 函数方式无法触发 watch ,这说明这种方式指定的是无效的数据源,那我们按官网的说明,使用数组格式来看看

watch([()=>person.name,()=>person.job],(newValue, oldValue) => {
    console.log("person变化了", newValue, oldValue);
  },{deep: true}
);

 

 测试证明,以数组格式包裹的getter函数是可以同时实现监听多个数据的。

除此之外,我们也可以配置 {immediate: true} 来实现初始化监听

watch([()=>person.name,()=>person.job],(newValue, oldValue) => {
    console.log("person变化了", newValue, oldValue);
  },{deep: true,immediate: true}
);

总结

  1. Vue3 中能够使用 Vue2 的模式来实现 watch监听 动作
  2. Vue3 中的 watch 因为是组合式api,所以也需要先引入再使用,和 computed 一致
  3. Vue3 中的 watch 是一个函数,接收三个参数:                                                                          参数一:需要监听的数据,参数类型可以是ref,响应式对象,getter函数,或数组
      参数二:监听的回调函数,接收两个参数,分别代表新值和旧值
      参数三:一个对象,包含复杂监听的配置,例如深度监听 ( dep:true ),初始化监听( immediate: true )等
  4. Vue3 中 setup 中的 watch 不用return出去,因为 watch 监听是动作,而 computed 计算属性最终返回的是值。
  5. Vue3 中 setup 中的 watch 也是一个函数,返回值也是一个函数,用来停止监听动作,可以通过变量接收之后调用该函数停止监听

  6. Vue3 的watch 存在几种情况需要注意:
    1. 监视 ref定义的【基本类型】数据:直接写数据名即可,监视的是其value值的改变。
    2. 监视ref定义的【对象类型】数据:直接写数据名,监视的是对象的【地址值】,若想监视对象内部的数据,要手动开启深度监视。
      • 若修改的是ref定义的对象中的属性,newValue 和 oldValue 都是新值,因为它们是同一个对象。
      • 若修改整个ref定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。
    3. 监视reactive定义的【对象类型】数据,且默认开启了深度监视
    4. 监视refreactive定义的【对象类型】数据中的某个属性,监视的要是对象里的属性,那么最好写函数式,注意点:若是对象监视的是地址值,需要关注对象内部,需要手动开启深度监视。
      • 若该属性值不是【对象类型】,需要写成函数形式。
      • 若该属性值是依然是【对象类型】,可直接编,也可写成函数,建议写成函数。
  7. 通过数组格式,可以同时监听多个数据,避免重复写多个单独的 watch
  8. reactive 定义的数据,是不允许整个对象直接替换的,只能通过 Object.assign 合并对象的方式对整个对象进行操作。因为替换之后相当于与源对象失去了响应式绑定。

    ref 定义的数据,可以通过 xxx.value 的形式来整体替换数据,因为 ref 返回的是一个 refImpl 对象,其中的 Value 才是响应式的值,所以可以只监听 Value 的改变

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐