vue2中watch出现过,但有vue3中多了一个watchEffect,同样是监听数据变化,watch和watchEffect区别有什么区别呢,下面用实例详细讲解。
watch 和 watchEffect 区别
我们已经大概知道了 watch 和 watchEffect 的用法,那么它们之间的区别相信大家也了解了一些,这里我们总结一下它们之间的区别。
watch 和 watchEffect 都能监听响应式数据的变化,不同的是它们监听数据变化的方式不同。
watch 会明确监听某一个响应数据,而 watchEffect 则是隐式的监听回调函数中响应数据。
watch 在响应数据初始化时是不会执行回调函数的,watchEffect 在响应数据初始化时就会立即执行回调函数。
1.watch 基本使用
在 Vue3 中的组合式 API 中,watch 的作用和 Vue2 中的 watch 作用是一样的,他们都是用来监听响应式状态发生变化的,当响应式状态发生变化时,都会触发一个回调函数。
使用语法:
import { watch } from "vue"
watch( name , ( curVal , preVal )=>{ //业务处理 }, options ) ;
共有三个参数,分别为:
name:需要帧听的属性;
(curVal,preVal)=>{ //业务处理 } 箭头函数,是监听到的最新值和本次修改之前的值,此处进行逻辑处理。
options :配置项,对监听器的配置,如:是否深度监听。
1.1 监听 ref 定义的响应式数据
代码:
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>{{ message }}</p>
<button @click="changeMsg">更改 message</button>
</template>
<script setup>
import { ref, watch } from "vue";
const message = ref("小猪课堂");
watch(message, (newValue, oldValue) => {
console.log("新的值:", newValue);
console.log("旧的值:", oldValue);
});
const changeMsg = () => {
message.value = "张三";
};
</script>
输出结果:

注意:前面我们一直强调 watch 监听的是响应式数据,如果我们监听的数据不是响应式的,那么可能会抛出如下警告:

1.2 监听 reactive 定义的响应式数据
<template>
<div>
<div>{{obj.name}}</div>
<div>{{obj.age}}</div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup(){
const obj = reactive({
name:'zs',
age:14
});
const changeName = () => {
obj.name = 'ls';
};
watch(obj,(newVal,oldVal) => {
console.log('值改变了',newVal,oldVal)
})
return {
obj,
changeName,
}
}
}
</script>

1.3 监听多个响应式数据数据
<template>
<div>
<div>{{obj.name}}</div>
<div>{{obj.age}}</div>
<div>{{count}}</div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { reactive, ref, watch } from 'vue';
export default {
setup(){
const count = ref(0);
const obj = reactive({
name:'zs',
age:14
});
const changeName = () => {
obj.name = 'ls';
};
watch([count,obj],() => {
console.log('监听的多个数据改变了')
})
return {
obj,
count,
changeName,
}
}
}
</script>

1.4 监听对象中某个属性的变化
<template>
<div>
<div>{{obj.name}}</div>
<div>{{obj.age}}</div>
<button @click="changeName">改变值</button>
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup(){
const obj = reactive({
name:'zs',
age:14
});
const changeName = () => {
obj.name = 'ls';
};
watch(() => obj.name,() => {
console.log('监听的obj.name改变了')
})
return {
obj,
changeName,
}
}
}
</script>

1.5 深度监听(deep)、默认执行(immediate)
<template>
<div>
<div>{{obj.brand.name}}</div>
<button @click="changeBrandName">改变值</button>
</div>
</template>
<script>
import { reactive, ref, watch } from 'vue';
export default {
setup(){
const obj = reactive({
name:'zs',
age:14,
brand:{
id:1,
name:'宝马'
}
});
const changeBrandName = () => {
obj.brand.name = '奔驰';
};
watch(() => obj.brand,() => {
console.log('监听的obj.brand.name改变了')
},{
deep:true,
immediate:true,
})
return {
obj,
changeBrandName,
}
}
}
</script>

2. watchEffect 的使用
watchEffect 也是一个帧听器,是一个副作用函数。它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听。
<template>
<div>
<input type="text" v-model="obj.name">
</div>
</template>
<script>
import { reactive, watchEffect } from 'vue';
export default {
setup(){
let obj = reactive({
name:'zs'
});
watchEffect(() => {
console.log('name:',obj.name)
})
return {
obj
}
}
}
</script>

停止侦听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
<template>
<div>
<input type="text" v-model="obj.name">
<button @click="stopWatchEffect">停止监听</button>
</div>
</template>
<script>
import { reactive, watchEffect } from 'vue';
export default {
setup(){
let obj = reactive({
name:'zs'
});
const stop = watchEffect(() => {
console.log('name:',obj.name)
})
const stopWatchEffect = () => {
console.log('停止监听')
stop();
}
return {
obj,
stopWatchEffect,
}
}
}
</script>

清除副作用
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (场景:有一个页码组件里面有5个页码,点击就会异步请求数据。于是做一个监听,监听当前页码,只要有变化就请求一次。问题:如果点击的比较快,从1到5全点了一遍,那么会有5个请求,最终页面会显示第几页的内容?第5页?那是假定请求第5页的ajax响应的最晚,事实呢?并不一定。于是这就会导致错乱。还有一个问题,连续快速点5次页码,等于我并不想看前4页的内容,那么是不是前4次的请求都属于带宽浪费?这也不好。
于是官方就给出了一种解决办法:
侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。
当以下情况发生时,这个失效回调会被触发:
副作用即将重新执行时;
侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
首先,异步操作必须是能中止的异步操作,对于定时器来讲中止定时器很容易,clearInterval之类的就可以,但对于ajax来讲,需要借助ajax库(比如axios)提供的中止ajax办法来中止ajax。
现在我写一个能直接运行的范例演示一下中止异步操作:
先搭建一个最简Node服务器,3000端口的:
服务端代码:
const http = require('http');
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', "*");
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS');
res.writeHead(200, { 'Content-Type': 'application/json'});
});
server.listen(3000, () => {
console.log('Server is running...');
});
server.on('request', (req, res) => {
setTimeout(() => {
if (/\d.json/.test(req.url)) {
const data = {
content: '我是返回的内容,来自' + req.url
}
res.end(JSON.stringify(data));
}
}, Math.random() * 3000);
});
前端
<template>
<div>
<div>content: {{ content }}</div>
<button @click="changePageNumber">第{{ pageNumber }}页</button>
</div>
</template>
<script>
import axios from 'axios';
import { ref, watchEffect } from 'vue';
export default {
setup() {
let pageNumber = ref(1);
let content = ref('');
const changePageNumber = () => {
pageNumber.value++;
}
watchEffect((onInvalidate) => {
// const CancelToken = axios.CancelToken;
// const source = CancelToken.source();
// onInvalidate(() => {
// source.cancel();
// });
axios.get(`http://localhost:3000/${pageNumber.value}.json`, {
// cancelToken: source.token,
}).then((response) => {
content.value = response.data.content;
}).catch(function (err) {
if (axios.isCancel(err)) {
console.log('Request canceled', err.message);
}
});
});
return {
pageNumber,
content,
changePageNumber,
};
},
};
</script>
上面注释掉的代码先保持注释,然后经过多次疯狂点击之后,得到这个结果,显然,内容错乱了:

现在取消注释,重新多次疯狂点击,得到的结果就正确了:

除了最后一个请求,上面那些请求有2种结局:
一种是响应的太快,来不及取消的请求,这种请求会返回200,不过既然它响应太快,没有任何一次后续 ajax 能够来得及取消它,说明任何一次后续请求开始之前,它就已经结束了,那么它一定会被后续某些请求所覆盖,所以这类请求的 content 会显示一瞬间,然后被后续的请求覆盖,绝对不会比后面的请求还晚。
另一种就是红色的那些被取消的请求,因为响应的慢,所以被取消掉了。
所以最终结果一定是正确的,而且节省了很多带宽,也节省了系统开销。
副作用刷新时机
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个$nextTick中多个状态改变导致的不必要的重复调用
同一个“tick”的意思是,Vue的内部机制会以最科学的计算规则将视图刷新请求合并成一个一个的"tick",每个“tick”刷新一次视图,如:a=1; b=2; 只会触发一次视图刷新。$nextTick的Tick就是指这个。
如 watchEffect 监听了2个变量 count 和 count2,当我调用countAdd,你觉得监听器会调用2次?
当然不会,Vue会合并成1次去执行。
代码如下,console.log只会执行一次:
<template>
<div>
<div>{{count}} {{count2}}</div>
<button @click="countAdd">增加</button>
</div>
</template>
<script>
import { ref,watchEffect } from 'vue';
export default {
setup(){
let count = ref(0);
let count2 = ref(10);
const countAdd = () => {
count.value++;
count2.value++;
}
watchEffect(() => {
console.log(count.value,count2.value)
})
return{
count,
count2,
countAdd
}
}
}
</script>
所谓组件的 update 函数是 Vue 内置的用来更新DOM的函数,它也是副作用,上文已经提到过。这时候有一个问题,就是默认下,Vue会先执行组件DOM update,还是先执行监听器?
<template>
<div>
<div id="value">{{count}}</div>
<button @click="countAdd">增加</button>
</div>
</template>
<script>
import { ref,watchEffect } from 'vue';
export default {
setup(){
let count = ref(0);
const countAdd = () => {
count.value++;
}
watchEffect(() => {
console.log(count.value)
console.log(document.querySelector('#value') && document.querySelector('#value').innerText)
})
return{
count,
countAdd
}
}
}
</script>
点击若干次(比如2次)按钮,得到的结果是:

为什么点之前按钮的innerText打印null?
因为事实就是默认先执行监听器,然后更新DOM,此时DOM还未生成,当然是null。
当第1和2次点击完,会发现:document.querySelector(‘#value’).innerText 获取到的总是点击之前DOM的内容。
这也说明,默认Vue先执行监听器,所以取到了上一次的内容,然后执行组件 update。
Vue 2其实也是这种机制,Vue 2使用 this.$ nextTick() 去获取组件更新完成之后的 DOM,在 watchEffect 里就不需要用this.$nextTick()(也没法用),有一个办法能获取组件更新完成之后的DOM,就是使用:
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
现在设上 flush 配置项,重新进入组件,再看看:

所以结论是,如果要操作“更新之后的DOM”,就要配置 flush: ‘post’。
如果要操作“更新之后的DOM ”,就要配置 flush: 'post'。 flush 取值: pre (默认) post (在组件更新后触发,这样你就可以访问更新的 DOM。这也将推迟副作用的初始运行,直到组件的首次渲染完成。) sync (与watch一样使其为每个更改都强制触发侦听器,然而,这是低效的,应该很少需要)
侦听器调试
onTrack 和 onTrigger 选项可用于调试侦听器的行为。
onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
onTrigger 将在依赖项变更导致副作用被触发时被调用。
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。
建议在以下回调中编写 debugger 语句来检查依赖关系:
watchEffect(
() => {
/* 副作用 */
},
{
onTrigger(e) {
debugger
}
}
)
onTrack 和 onTrigger 只能在开发模式下工作。
3. 总结
watch 特点
watch 监听函数可以添加配置项,也可以配置为空,配置项为空的情况下,watch的特点为:
有惰性:运行的时候,不会立即执行;
更加具体:需要添加监听的属性;
可访问属性之前的值:回调函数内会返回最新值和修改之前的值;
可配置:配置项可补充 watch 特点上的不足:
immediate:配置 watch 属性是否立即执行,值为 true 时,一旦运行就会立即执行,值为 false 时,保持惰性。
deep:配置 watch 是否深度监听,值为 true 时,可以监听对象所有属性,值为 false 时保持更加具体特性,必须指定到具体的属性上。
watchEffect 特点
非惰性:一旦运行就会立即执行;
更加抽象:使用时不需要具体指定监听的谁,回调函数内直接使用就可以;
不可访问之前的值:只能访问当前最新的值,访问不到修改之前的值;


