目录
一:什么是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()的执行时间是比手写深拷贝要久的,因此在大项目中,手写的执行效率往往更具有优势。