目录
一:什么是watch?
二:watch的基础使用
1.最基本的使用
2.简写形式
三:watch中的immediate和deep属性
1.immediate属性
2.deep属性
3.解决深度监听新旧值相同的问题
1)使用序列化和反序列化。
2)手写深拷贝算法
一:什么是watch?
相信大家在开发项目中,有时候会遇到一些需求,是当一个数据改变之后进行一些操作,这个时候有些人会设置一个定时器,周期性的去循环访问,当发现数据发生了改变后执行操作。但是这种操作方式会导致系统资源的浪费,以及更新的不及时等。因此vue通过watch(侦听器)提供了一个更通用的方法来响应数据的变化,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
核心原理就是对传入Vue options的watch对象里面的所有的属性,都添加一个自定义watcher,收集对应属性的依赖,然后当这个依赖项更新的时候,不仅通知它的渲染watcher(可能没有),也会通知当前的这个自定义watcher,从而叫到你定义的callback,完成函数内的逻辑。
二:watch的基础使用
1.最基本的使用
下方代码是watch的一个基础使用,可以看到当第一次进入页面的时候,并没有触发,而点击按钮的时候,name的值发生了改变,被watch监听到,因此执行了下面的输出逻辑。
<template>
<div class="home">
<el-button @click="name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
name: '张三'
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
name: { //这里是要监听的变量名
handler(newValue, oldValue) { //这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue)
console.log('原来的值是:' + oldValue)
}
}
}
}
</script>
2.简写形式
当只是如上所示,简单地进行监听的时候,为了方便,我们可以直接使用简写形式。
<template>
<div class="home">
<el-button @click="name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
name: '张三'
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
name(newValue, oldValue) { //这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue)
console.log('原来的值是:' + oldValue)
}
}
}
</script>
三:watch中的immediate和deep属性
1.immediate属性
在上述代码中,我们可以发现,当watch第一次绑定数据的时候并没有触发监听效果,这是watch中的一个特性,但同样的,我们可以使用immediate属性改变这种特性。
<template>
<div class="home">
<el-button @click="name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
name: '张三'
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
name: {
handler(newValue, oldValue) {//这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue)
console.log('原来的值是:' + oldValue)
},
immediate: true
}
}
}
</script>
2.deep属性
在使用watch的过程中,如果定义了一个对象,这时候我们会发现使用一般的watch监听,是无法监听对象内部的改变的,这时候我们需要使用depp属性进行一个深度监听。
比如在以下代码中,因为watch所监听的是一个person对象,当点击按钮改变之后,会发现没有任何执行效果。也就是监听失败。
<template>
<div class="home">
<el-button @click="person.name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name:'张三'
}
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
person: {
handler(newValue, oldValue) {//这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue.name)
console.log('原来的值是:' + oldValue.name)
}
}
}
}
</script>
所以我们在上述代码中加入depp:true,形成下面的代码,这时候我们可以在控制台看到是能够成功执行的。
<template>
<div class="home">
<el-button @click="person.name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name:'张三'
}
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
person: {
handler(newValue, oldValue) {//这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue.name)
console.log('原来的值是:' + oldValue.name)
},
deep:true//深度监听
}
}
}
</script>
3.解决深度监听新旧值相同的问题
但是在上面的运行效果中,我们可以发现改变前和改编后的值都是一样的,这显然不符合我们的初衷,失去了oldValue的作用,当然我们可以把监听的字符串person变成’person.name‘,但是这样又不属于监听对象了,而是回到了最初的监听字符串。
我查了一下vue的官方文档,对watch的解释如下
在变异 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变异之前值的副本。
通过官方解释,我们有以下两种方式解决该问题:
1)使用序列化和反序列化。
<template>
<div class="home">
<el-button @click="person.name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name:'张三'
}
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
newPerson: {
handler(newValue, oldValue) {//这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue.name)
console.log('原来的值是:' + oldValue.name)
},
deep:true,//深度监听
}
},
computed:{
// 使用计算属性进行深拷贝
newPerson(){
return JSON.parse(JSON.stringify(this.person))
}
}
}
</script>
这种方式优点是直接简单,对于程序员而言代码行数更少,但是缺点在于这种深拷贝方式容易带来性能的问题,并且会丢失原有的原型链,在如unidentified等特殊值时会报错,将NaN序列化成null等问题。所以通常而言,用第二种方法会更好一些
2)手写深拷贝算法
<template>
<div class="home">
<el-button @click="person.name='李四'">改变</el-button>
</div>
</template>
<script>
export default {
data() {
return {
person: {
name: '张三'
}
}
},
//使用watch监听器,当不点击按钮的时候不会触发
watch: {
newPerson: {
handler(newValue, oldValue) { //这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue.name)
console.log('原来的值是:' + oldValue.name)
},
deep: true, //深度监听
}
},
computed: {
// 使用计算属性进行深拷贝
newPerson() {
// 使用自己手写的深克隆方法,效率更高
return this.deepClone(this.person)
}
},
methods: {
//下面这个是手写深拷贝
deepClone(source) {
let target = null;
if (!this.isArray(source) && !this.isObject(source)) {
target = source;
}
if (this.isArray(source)) {
target = [];
for (let i = 0; i < source.length; i++) {
target[i] = this.deepClone(source[i]);
}
}
if (this.isObject(source)) {
target = {};
for (let key in source) {
target[key] = this.deepClone(source[key]);
}
}
return target;
},
isArray(value) {
return Object.prototype.toString.call(value) === '[object Array]';
},
isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
}
}
</script>
这里运行效果和方案一的效果是一样的,所以不再附加运行截图。但是运行时间上可以看到,将点击事件的代码修改成方法,并且使用console.time()来计算时间,如下代码所示
<template>
<div class="home">
<!-- 这里的@click直接变成一个方法 -->
<el-button @click="handleClick">改变</el-button>
</div>
</template>
<!-- script修改部分如下 -->
<script>
export default {
...
watch: {
newPerson: {
handler(newValue, oldValue) { //这里的第一个参数是改变后的值,第二个参数是原来的值
console.log('改后的值是:' + newValue.name)
console.log('原来的值是:' + oldValue.name)
console.timeEnd('process')// 《---新增代码(关闭计时器)
},
deep: true, //深度监听
}
},
...
},
methods: {
handleClick(){// 《---新增代码
console.time('process')//打开计时器
this.person.name='李四'
},
....
}
}
</script>
添加上述代码后,再次执行两种方式的代码,执行时间如下
通过上面两张图的对比,我们可以清楚地看到,使用JSON.parse()的执行时间是比手写深拷贝要久的,因此在大项目中,手写的执行效率往往更具有优势。