在日常的开发中,很多时候我们需要去对一些状态进行监听,比如当显示学生的成绩列表时,我们使用一个学生的学号student_num作为请求成绩的参数,如果没有监听机制,当学号student_num改变时,我们需要依赖用户的操作去刷新成绩。但是有了侦听器,我们可以通过侦听器去监听student_num,当其发生改变时,自动去执行成绩刷新的操作。省去了很多冗余的逻辑。在Vue中,侦听器主要有两个,watch和watchEffect。下文将主要介绍他们的用法和区别。
我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数,下面的代码中,我们监听一个name属性的值,使用一个输入框输入name的不同值,当name的值改变时触发回调,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>watch侦听器的使用</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const app = Vue.createApp({
setup(props,context){
const {ref,watch} = Vue;
const name = ref('walt');
// 具备懒加载
// 参数可以拿到原始值和当前值
// 用一个侦听器可以监听多个数据
watch(name,(currentValue,prevValue)=>{
console.log(currentValue,prevValue);
})
return {name}
},
template:
`
Name:
`
});
const vm = app.mount('#root');
</script>
从上面的代码中我们可以看到,传入的第一个参数name使用了ref包装,使其具有了响应式的属性,其实,watch 的第一个参数可以是不同形式的“数据源”:它不仅可以是一个 ref (包括计算属性)、还可以是一个响应式对象、一个 getter 函数、或多个数据源组成的数组,如下所示:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)
// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
注意:我们不能直接监听对象的属性,比如下列的做法是不对的
// 不正确的写法
setup(props,context){
const {watch,reactive,toRefs} = Vue;
const nameObj = reactive({name:'walt'});
watch(nameObj.name,(currentValue,prevValue)=>{
console.log(currentValue,prevValue);
})
const {name} = toRefs(nameObj)
return {name}
}
如上面的代码所示,直接监听对象的属性是不行的,需要提供一个返回该属性的getter函数,正确的写法如下所示:
// 正确写法
setup(props,context){
const {watch,reactive,toRefs} = Vue;
const nameObj = reactive({name:'walt'});
watch(()=>nameObj.name,(currentValue,prevValue)=>{
console.log(currentValue,prevValue);
})
const {name} = toRefs(nameObj)
return {name}
}
这里引用官网的例子解释, 直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
我们可以添加deep :true
,来将上面的例子转换成深层侦听器,代码如下:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
需要注意的是,深层侦听器会遍历被侦听对象的嵌套属性,为了性能考虑,谨慎使用
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。比如,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行,代码如下:
watch(source, (newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })
watchEffect帧听器是立即执行的,它不像watch是懒加载的,而且它不需要传递要侦听的内容,会自动感知代码依赖,不需要传递很多参数,只需要传递一个回调函数就可以。引用Vue官网的例子,比如:当 todoId 的引用发生变化时使用侦听器来加载一个远程资源:
const todoId = ref(1)
const data = ref(null)
watch(todoId, async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
}, { immediate: true })
上面的todoId一次是作为源,另一次是在回调中使用的,可以用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
使用watchEffect,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),不再需要明确传递 todoId 作为源值
停止侦听器
在 setup() 或
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,不需要关心怎么停止一个侦听器。侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,必须手动停止,以防内存泄漏。如下方这个例子:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const stop= watchEffect(() => {})
// ...当该侦听器不再需要时
stop()
至此,本文已经介绍完了watch和watchEffect帧听器的使用和注意点,最后总结下两个侦听器的区别,watch具备懒加载,参数可以拿到原始值和当前值,用一个侦听器可以监听多个数据;而watchEffect侦听器是立即执行的,不需要传递要侦听的内容,它会自动感知代码依赖,但是watchEffect无法获取改变之前的值