- Published on
watch 与 watchEffect
- Authors
- Name
- Deng Hua
目录
watch 用法
监听 ref 定义的响应式数据
<template>
<div>
<div>value:{{count}}</div>
<button @click="add">change</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup(){
const count = ref(0);
const add = () => {
count.value ++
};
watch(count,(newVal,oldVal) => {
console.log('The value has changed:',newVal,oldVal)
})
return {
count,
add,
}
}
}
</script>
监听 reactive 定义的响应式数据
当直接侦听一个响应式对象时,侦听器会自动启用深层模式。
<template>
<div>
<div>{{ obj.name }}</div>
<div>{{ obj.age }}</div>
<button @click="changeName">change</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
const obj = reactive({
name: "zs",
age: 14,
});
const changeName = () => {
obj.name = "ls";
};
watch(() => obj.name, (newVal, oldVal) => {
console.log("The value has changed:", newVal, oldVal);
});
</script>
监听多个反应数据
<template>
<div>
<div>{{ obj.name }}</div>
<div>{{ obj.age }}</div>
<div>{{ count }}</div>
<button @click="changeName">change</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
let count = ref(0);
const obj = reactive({
name: "zs",
age: 14,
});
const changeName = () => {
obj.name = "ls";
count.value++;
};
watch([count, () => obj.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(newCount); // 1
console.log(oldCount); // 0
console.log(newName); // ls
console.log(oldName); // zs
});
</script>
监听响应式对象某一属性的变化。
<template>
<div>
<div>{{ obj.name }}</div>
<div>{{ obj.age }}</div>
<button @click="changeName">change</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
const obj = reactive({
name: "zs",
age: 14,
});
const changeName = () => {
obj.name = "ls";
};
watch(
() => obj.name,
() => { console.log("The obj.name being monitored has changed.")}
);
</script>
deep、immediate
<template>
<div>
<div>{{ obj.brand.name }}</div>
<button @click="changeBrandName">change</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
const obj = reactive({
name: "zs",
age: 14,
brand: {
id: 1,
name: "bwm",
},
});
const changeBrandName = () => {
obj.brand = {
id: 2,
name: "aaa",
};
};
watch(
() => obj.brand,
(newVal, oldVal) => {
console.log(newVal.name); // aaa
console.log(oldVal?.name); // bwm
console.log(newVal.id); // 2
console.log(oldVal?.id); // 1
},
{ deep: true, immediate: true }
);
</script>
watchEffect 用法
基本
<template>
<div>
<input type="text" v-model="obj.name" />
</div>
</template>
<script setup lang="ts">
import { reactive, watchEffect } from "vue";
let obj = reactive({
name: "zs",
});
setTimeout(() => {
obj.name = "ls";
}, 2000);
watchEffect(() => {
console.log("name:", obj.name); // name: zs
// and 1s after print 'name: ls'
});
</script>
停止 watchEffect
<template>
<div>
<input type="text" v-model="obj.name" />
</div>
</template>
<script setup lang="ts">
import { reactive, watchEffect } from "vue";
let obj = reactive({
id: 1,
});
const intervalId = setInterval(() => {
obj.id++;
}, 1000);
const stop = watchEffect(() => {
console.log("id:", obj.id);
if (obj.id > 5) {
stop();
clearInterval(intervalId);
}
});
/**
print:
id: 1
id: 2
id: 3
id: 4
id: 5
id: 6
*/
</script>
watchEffect 的清除副作用
有时Effect函数可能会执行一些异步副作用,这些副作用在无效时可能需要清除。
一个场景:有一个分页组件,含有5个页面,点击它会异步请求数据。所以设置一个监听器,监听当前页码,只要有变化就发出请求。
问题:如果点击得非常快,一口气从第1页到第5页,这将发出5个请求,那么最终会显示哪一页呢?第5页吗?这假定第5页请求的 ajax 响应是最后收到的,但真的是这样吗?未必。所以这可能会造成混乱。
另一个问题,如果您连续快速点击分页数字5次,基本上您对前4页的内容不感兴趣,那么这些页面的前4个请求的带宽是否都被浪费掉了,这也不太好。
官方的解决方案是,监听器副作用函数可以接受一个 onInvalidate
函数作为参数,可以注册该函数用于在回调无效时清除回调。当发生以下情况时,会触发该失效回调:
副作用即将重新执行时
侦听器已停止((如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则会在组件卸载时进行)。
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// Id已变化 或者 监听器 stop()。
// 使先前挂起的异步操作失效
token.cancel()
})
})
异步操作必须是可中止的。对于计时器来说,要停止它们非常简单,如clearInterval
这样的函数就可以做到。但对于ajax来说,那就需要使用ajax库(如axios)提供的abort ajax方法来中止ajax。
示例
以下代码用于演示中止异步操作
首先,创建一个简单的Node服务器:
import http from '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(2000, () => {
console.log('Server is running...');
});
server.on('request', (req, res) => {
setTimeout(() => {
if (/\d.json/.test(req.url)) {
const data = {
content: 'I am the returned content from' + req.url
}
res.end(JSON.stringify(data));
}
}, Math.random() * 2000);
});
接下来是使用该server的Vue组件:
<template>
<div>
<div>content: {{ content }}</div>
<button @click="changePageNumber">Page {{ pageNumber }}</button>
</div>
</template>
<script setup lang="ts">
import axios from "axios";
import { ref, watchEffect } from "vue";
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:2000/${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);
}
});
});
</script>
未注释source.cancel()
之前,连续点击请求,每个请求都不会被取消,当由于接口响应时间随机,导致最终的请求结果并不一定就是最后一次点击时的结果。
放开注释source.cancel()
后,最终的结果一定是正确的,同时也节省了大量的带宽,减少了并发。
副作用刷新的时机
Vue 的响应式系统会缓存副作用函数并异步刷新它们,这可以防止同一个 tick 中的多个状态变化导致不必要的重复调用。
tick 的含义是,Vue 的内部机制将根据最科学的计算规则将视图刷新的请求合并为一个 tick。 每个 "tick" 刷新视图一次,例如 a=1;b=2;
仅会触发一次视图刷新。$nextTick
中的 "Tick" 就指的是这个。
例如, watchEffect 正在侦听两个变量 count
和 count2
。当我调用 countAdd
时,你认为监听器会被调用两次吗?
<template>
<div>
<div>{{count}} {{count2}}</div>
<button @click="countAdd">Increase</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>
当然不是,Vue 会将其合并到一个执行中。 console.log
只会执行一次。
在Vue的核心实现中,组件的更新函数也是一个被监听的副作用。当用户定义的副作用函数进入队列时,默认情况下它会在所有组件更新之前执行。
所谓组件更新函数,就是Vue内置的一个函数,用来更新DOM,这也是一个副作用。
此时有一个问题: 默认情况下,Vue 会先执行组件 DOM 更新,还是先执行监听器?换句话说,监听器是在DOM更新之前执行?还是之后?
<template>
<div>
<div id="value">{{ count }}</div>
<button @click="countAdd">Increase</button>
</div>
</template>
<script setup lang="ts">
import { ref, watchEffect } from "vue";
let count = ref(0);
const countAdd = () => {
count.value++;
};
watchEffect(() => {
console.log(count.value);
console.log(document.querySelector("#value") && document.querySelector("#value")?.innerText);
});
// result
// before click
// 0
// null
// first click
// 1
// 0
// second click
// 2
// 1
</script>
未点击click
前,打印innerText
,值为null
,我们知道了,默认先执行监听器,后更新DOM。
首次运行时,虽然count.value
为1
了,但是Vue还没有进行DOM更新操作,最新的DOM还没有生成,所以自然innerText
为null
。
且在第一次和第二次点击后,发现document.querySelector('#value').innerText
总是获取点击前的DOM的内容。
这也说明,默认情况下,Vue会先执行监听器,所以才到取上次的内容,然后再进行组件更新。
Vue 2实际上也使用了这种机制。 Vue 2 在组件更新后使用 this.$nextTick()
来获取 DOM。
在 watchEffect
中,不需要使用 this.$nextTick()
(也不能),但是有一种方法可以在组件更新后获取 DOM,那就是配置flush
:
// 它在组件更新后触发,因此可以访问更新后的DOM。
// 注意:这也会延迟Effect的初始执行,直到第一个组件渲染完成。
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
// result
// before click
// 0
// 0
// first click
// 1
// 1
// second click
// 2
// 2
总结: 默认情况下,先执行 watchEffect 监听器,然后执行 DOM 更新。如果要对“更新后的 DOM”进行操作,则需要配置 flush: 'post'
。
flush
选项具有以下几个值:
pre
(默认)post
(组件更新后触发,这样就可以访问更新后的 DOM。这也会延迟副作用的首次执行,直到第一个组件渲染完成)sync
(与watch
一样,强制侦听器每次更新后触发,但效率低下,很少使用)
调试 watchEffect 监听器
onTrack
和 onTrigger
可用于调试侦听器。
当响应式属性或ref作为依赖项被跟踪时, onTrack
被调用。
当依赖项更改触发副作用时,将调用 onTrigger
。
两个回调都会收到一个debugger事件,其中包含有关依赖项的信息
建议在以下回调中编写 debugger
语句来检查依赖关系:
watchEffect(
() => {
/* side effects */
},
{
onTrigger(e) {
debugger
}
}
)
onTrack
和 onTrigger
仅在开发模式下工作。
End.