上一节说到了 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 是一个函数,接收三个参数,
- 第一个参数是需要被监听的数据( 单个数据,数组格式的多个数据),
- 第二个参数是回调函数,用来处理监听之后的动作
- 第三个参数则是监听配置项( 深度监听、初始化监听 )。
但是和 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>
Vue3 中 watch 的复杂使用方式
上面说的Vue3 中 watch 的简单使用方式,其实就是监听单个 ref 定义的响应式数据。但是 Vue3 中的 watch 可以分为好几种情况:
情况一:通过 watch 监听 ref 定义的单个基础类型响应式数据,也就是上面的例子
情况二:通过 watch 监听 ref 定义的多个基础类型响应式数据,例如
<template>
<h2>当前求和值为:{{ sum }}</h2>
<button @click="sum++">点击加1</button>
<br>
<h2>当前msg值为:{{ msg }}</h2>
<button @click="msg += '!'">点击加!</button>
</template>
<script>
import { ref ,watch} from "vue";
export default {
name: "TestComponent",
setup() {
let sum = ref(1);
let msg = ref('你好啊')
watch(sum, (newValue, oldValue) => {
console.log("sum的值变化了", newValue, oldValue);
})
watch(msg, (newValue, oldValue) => {
console.log("msg的值变化了", newValue, oldValue);
})
return {
sum,
msg
};
},
};
</script>
但是这么写很明显太麻烦了,我想监听多个,那我就需要写多个 watch 监听函数,还不如 Vue2的配置项直接定义一个对象来的方便,所以Vue3 也提供了简便写法,那就是通过数组形式一次性监听多个数据:
// 通过 [sum,msg] 一次性监听多个数据
watch([sum,msg], (newValue, oldValue) => {
console.log("sum或msg的值变化了", newValue, oldValue);
})
同时,我们改变 sum和msg,发现返回的 newValue 和 oldValue 分别是两个数组
- 第一步:改变 sum ,newValue 数组中 sum 值改变,msg值不变,oldValue 数组中的值就是 sum 和 msg 的初始值
- 第二步:改变 msg,newValue 数组中 sum 值不变,msg值改变变,oldValue 数组中的值就是 sum 和 msg 的上一次的值
情况三:通过 watch 中的 immediate: true 初始化监听 ref 定义的基础类型响应式数据
watch(sum, (newValue, oldValue) => {
console.log("sum的值变化了", newValue, oldValue);
},{immediate: true})
可以发现,初始化监听成功,在组件初始化, sum 未发生改变时 监听动作就已经执行了。
情况四:通过 watch 监听 ref 定义的对象类型的响应式数据 -- 存在bug( 无法正确获取 oldValue )
我们用 ref 定义一个响应式对象数据,但是这两有两个点需要注意:
- 通过 ref 定义的对象数据,其实底层还是通过 reactive 来实现响应式的
- 通过 ref 定义的数据是一个 RefImpl对象,在 js 代码中使用时,不会自动解包,需要 .value
<template>
<p>{{person.name}}</p>
<p>{{person.age}}</p>
<button @click="person.name += '~'">更改name</button>
<button @click="person.age++">更改age</button>
</template>
export default {
name: "TestComponent",
setup() {
let person = ref({
name: "al",
age: 28,
});
watch(person.value, (newValue, oldValue) => {
console.log("person的值变化了", newValue, oldValue);
});
return {
person,
};
},
};
然后我们就会发现下面这么问题:
- 当更改 name 属性时,newValue 中的 name 改变了,但是 oldValue 中的 name 也同步变了
- 当更改 age 属性时,newValue 中的 age改变了,但是 oldValue 中的 age也同步变了
这和我们想的也太不一样了啊,不是说好了 oldValue 是上一次的值么,怎么还同步更新了呢? 这其实就是 Vue3 的监听bug,暂时官方也没有给出具体的解决办法,但是其实我们在开发过程中也确实不太关注 oldValue 的值。
但是如果你一定想要监听的话,建议把对象拆成单个 ref ,或者把你需要监听的对象中的某个属性单独拆成一个 ref,例如:上面的例子中,我现摘指向单独监听 age,那我就不把age 塞到 person 对象里面了,单独拎出来使用 ref定义。
let person = ref({
name: "al",
});
let age = ref(28)
watch(age.value, (newValue, oldValue) => {
console.log("age的值变化了", newValue, oldValue);
});
如果你觉得 reactive 可以解决这个问题,你也不妨试一试通过 reactive 定义的响应式数据能否达到你想要的效果。
关于 reactive 和 ref 的原理及对比请参考之前的博文 -- Vue2与Vue3响应式原理对比 里面通过源码详细解释了 ref 和 reactive 定义对象数据的底层关系
情况五:深度监听 ref 定义的对象形式的响应式数据 -- 默认开启 deep:true。监听整个对象。可通过配置关闭( 但是在 Vue3.2之前的版本,好像是不能进行配置更改的 )
其实我们按照上面的例子,将数据嵌套多几层,然后改变数据
let person = ref({
name: "al",
age: 28,
job:{
j1: {
work: '前端',
salary:1
}
}
});
watch(person.value, (newValue, oldValue) => {
console.log("person.salary的值变化了", newValue, oldValue);
});
然后改变数据
<template>
<p>{{ person.job.j1.work }}</p>
<p>{{ person.job.j1.salary }}</p>
<button @click="person.job.j1.salary++">涨薪</button>
</template>
我们可以发现,此时 深度嵌套的值也能被监听到。但是 在 Vue2 中监听深度嵌套数据时,我们是需要配置 deep:true 才能实现的 ,但是在 Vue3 中不用开启 deep:true 就可以直接实现深度监听了。
所以我们可以大胆假设,在 Vue3 中,deep:true 是默认开启的,如果我们进行配置关闭,那会有什么结果呢?
watch(person.value, (newValue, oldValue) => {
console.log("person.salary的值变化了", newValue, oldValue);
},{deep:false}); // 关闭深度监听
关闭之后我们可以发现,无法监听到深度嵌套数据的改变了。
但是,有一个问题需要说明,因为我的 Vue 版本是 "vue": "^3.2.13" 所以看起来配置 是生效的。但是如果好像是3.2以下的版本,这个配置是不生效的,只能默认开启 deep:true。
情况六:监听 ref 定义的对象形式的响应式数据 -- 只监听嵌套对象中的某个基础属性
<template>
<p>工作:{{ person.age }}</p>
<button @click="person.age++">改变年龄</button>
</template>
let person = ref({
name: "al",
age: 28,
});
watch(person.value.age, (newValue, oldValue) => {
console.log("person.age改变了", newValue, oldValue);
});
但是点击按钮之后,我们发现 控制台上并没有打印出数据,按理来说这是肯定不可能的,Vue肯定会放开这个口子供开发者使用,所以我们需要从使用语法中查找问题。
// 之前watch 的第一个参数,要么是 变量,要么是数组。
// 但是在这里,我们需要使用一个函数,通过函数的返回值来表明我们需要监听对象中的那个属性
watch(() => person.value.age, (newValue, oldValue) => {
console.log("person.age改变了", newValue, oldValue);
});
然后我们就会发现,控制台上已经可以正常打印正确数据了。
情况七:监听 ref 定义的对象形式的响应式数据 -- 只监听嵌套对象中的某些属性
按照情况二和情况七的结合,我们可以大致上了解监听嵌套对象中的某些属性应该怎么做。
// 通过 数组模式和函数返回模式相结合,实现了监听对象中某些属性
watch([() => person.value.name,() => person.value.age], (newValue, oldValue) => {
console.log("name或age改变了", newValue, oldValue);
});
情况八:监听 ref定义的对象形式的响应式数据 -- 深度监听嵌套对象中的值为复杂类型数据的属性
之前的几种监听情况大致上可以分为
- 监听 ref 定义的基础类型数据
- 监听 ref 定义的对象类型数据 --
- 监听的是整个对象( 包括深度嵌套对象 )
- 监听的是对象( 包括深度嵌套对象 )中的某个属性 -- 基础类型属性
- 监听的是对象( 包括深度嵌套对象 )中的某几个属性 -- 都是基础类型属性
如果我们现在监听的是 深度嵌套对象中某个值为复杂类型数据的属性时,那需要怎么做呢?
我们还是用之前的数据来进行测试
let person = ref({
name:'al',
job:{
j1: {
work: '前端',
salary:1
}
}
});
然后我们按照 情况六的模式来进行监听
<template>
<p>工作:{{ person.job.j1.salary }}</p>
<button @click="person.job.j1.salary++">涨薪</button>
</template>
// 监听属性通过函数返回
watch(() => person.value.job, (newValue, oldValue) => {
console.log("job中的数据该变了", newValue, oldValue);
});
发现此时并没有监听成功。按理来说这也不应该啊,我是按照之前的情况实现的啊,只不过是把监听的数据类型该变了,怎么就不行了呢?
此时我们回忆一下,在我们监听完整的对象时,默认开启 deep:true 。但我们监听的是整个对象,而不是对象中的某个属性。
如果我们监听的是对象中的某个对象属性时,这一套就不适用了,所以我们需要手动配置 deep:true
watch(() => person.value.job, (newValue, oldValue) => {
console.log("name或age改变了", newValue, oldValue);
},{deep: true});
此时改变 salary 后,控制台上能打印出数据,表atch功
watch 监听 ref 与 reactive 定义的数据使用方式区别
在之前的例子中我们定义数据都是使用的 ref, 虽然我有说 ref 定义的对象类型实际上底层还是通过 reactive 来实现响应式,但在 watch 监听时,使用方式还是存在区别的。
分别用 ref 和 reactive 定义深层嵌套的响应式数据
let person1 = ref({
job: {
j1: {
work: "前端",
salary: 1,
},
},
});
let person2 = reactive({
job: {
j2: {
work: "后端",
salary: 2,
},
},
});
然后使用 watch 分别监听。这里我们就可以看出区别在哪了。
watch(person1.value,(newValue, oldValue) => {
console.log("j1中salary改变了", newValue, oldValue);
},
);
watch(person2,(newValue, oldValue) => {
console.log("j2salary改变了", newValue, oldValue);
},
);
ref 定义的数据需要解包,而 reactive 定义的数据则可以直接使用。这一点可以在 之前的博文 ref函数与reactive函数的对比 中可以详细了解一下。
这是因为,通过 ref 转化的复杂数据,是一个 refImpl实例对象,其中 value属性的值才是真的响应式数据,是通过 reactive 方法中的 Proxy 代理对象实现响应式。这相比 reactive 直接转化就多了一层 refImpl 实例。所以在 js脚本中使用数据时,需要 .value 解包。
然后,我们参考一下情况八,其实对于 ref 定义的深层嵌套对象我们也可以通过手动配置 deep:true 的形式来实现深度监听
watch(person1,(newValue, oldValue) => {
console.log("j1中salary改变了", newValue, oldValue);
},
{ deep: true }
);
总结
- Vue3 中能够使用 Vue2 的模式来实现 watch监听 动作
- Vue3 中的 watch 因为是组合式api,所以也需要先引入再使用,和 computed 一致
- Vue3 中的 watch 是一个函数,接收三个参数: 参数一:需要监听的数据,参数类型可以是变量,数组或函数
参数二:监听的回调函数,接收两个参数,分别代表新值和旧值
参数三:一个对象,包含复杂监听的配置,例如深度监听 ( dep:true ),初始化监听( immediate: true ) - Vue3 中 setup 中的 watch 不需要使用变量接收,也不用返回,因为 watch 监听是动作,而 computed 计算属性最终返回的是值
- Vue3 的watch 存在几种情况需要注意:
- 通过 ref 定义的简单类型数据,可以直接实现监听,无bug -- 情况一 & 情况二 & 情况三
- 通过 ref 或 reactive 定义的复杂类型数据在监听时存在bug:
- 无法正确获得 oldValue -- 情况四
- 监听的是完整对象,则强制开启深度监听 -- 情况五
- 监听的是对象中的某个基础数据类型属性,则需要通过函数返回该属性 -- 情况六 & 情况七
- 监听的是对象中的某个值为复杂类型数据的属性,则需要通过函数返回,且需要手动配置 deep:true 实现深度监听 -- 情况八
- Vue3 中的watch 在监听 ref 和 reactiv定义的响应式数据时,使用方式是存在区别的,那就是 ref 定义的数据,在使用时是需要解包的,而 reactive 则是不需要。