侦听器 学习笔记
侦听器 学习笔记
一.介绍
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch()函数在每次响应式状态发生变化时触发回调函数:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.indexOf('?') > -1) {
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
二.watch()
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
先搞段代码感受一下:
<template>
<div>
响应式数据:<input v-model="username">
<hr>
响应式数据:<input v-model="user.name">
<hr>
<button @click="changeName">按钮</button>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from "vue";
let username = ref("wsy")
let user = reactive({name: "wsy", age: 111})
const changeName = () => {
username.value = "改了"
user.name = "改了"
}
//一个 ref
watch(username, (newValue, oldValue) => {
console.log("ref新值:" + newValue + " -- ref旧值:" + oldValue)
})
//一个 响应式对象
watch(user, (newValue, oldValue) => {
console.log("响应式对象新值:" + newValue.name + " -- 响应式对象旧值:" + oldValue.name)
})
//一个 函数返回一个值
watch(()=>user.name, (newValue, oldValue) => {
console.log("函数返回一个值新值:" + newValue + " -- 函数返回一个值旧值:" + oldValue)
})
//多个数据源
watch([username,()=>user.name],([newValue1,newValue2],[oldValue1,oldValue2]) => {
console.log("多数据源监听新值:" + newValue1 + " -- 多数据源监听旧值:" + oldValue1)
console.log("多数据源监听新值:" + newValue2 + " -- 多数据源监听旧值:" + oldValue2)
})
</script>
当点击按钮改变了数据的时候,就会触发watch()的内容。嗯就像你想继承三百亿一样,保证条件,当然有条件!下面来说说游戏规则:
-
详细信息
watch()默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。第一个参数是侦听器的响应式数据源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
immediate:在侦听器创建时立即触发回调。第一次调用时旧值是undefined。deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()。onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。
与
watchEffect()相比,watch()使我们可以:- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
一脸懵逼? 没关系,你们还有我,我还有大满老师~我把人话给搬过来。
2.1 参数
上面一坨说的就是二爷遗嘱上说的继承三百亿的前提条件,下面我代表老金说点容易犯错的地方:
2.1.1 第一个参数
-
这个源,必须得是响应式的,那个函数也不例外,你把上面的user改成这样:
let user = {name: "wsy", age: 111},就算用函数形式,也不行 -
源是响应式
对象,注意这个对象://这么搞就是错的,这叫对象的属性,不叫对象 watch(user.name, (newValue, oldValue) => { console.log("新值:" + newValue + " --旧值:" + oldValue) })
2.1.2 第二个参数
第二个参数是个回调,回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。新值和旧值没啥说的,就拿出来直接用就行了,问题是这个用于注册副作用清理的回调函数!
哦这名字,太JB搞事情了API啥时候能说点人话,我也不太懂,不过我从运行的结果来看,大致就是:
第一次进watch的时候不执行这个函数,第二次会先执行一下这个函数。目的可以就是判断一下你上次的操作执行完了没?如果没执行完你想怎么个处理办法。
//实验回调的第三个参数
watch(username, (newValue, oldValue, onCleanup) => {
onCleanup(()=>{
console.log("清理之前没执行完的异步操作")
})
console.log("新:" + newValue + " -- 旧:" + oldValue)
})
//控制台输出
新:改了 -- 旧:wsy
清理之前没执行完的异步操作
新:改了1 -- 旧:改了
清理之前没执行完的异步操作
新:改了12 -- 旧:改了1
2.1.3 第三个参数
第三个参就是个配置对象,其中有个比较骚的deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。
不得不说官网说的既严谨又随意。。严谨到我看不懂,随意到我按照他便面的意思一写就错。
首先:直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——--该回调函数在所有嵌套的变更时都会被触发:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
Tips:人话:传入一个响应式对象,会默认给你加个
deep:true,但是请注意:这个对象只能是reactive的,你换成ref的就不行
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调,或者显式地加上 deep 选项,强制转成深层侦听器:
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)
Tips:人话:监听函数,不给你加
deep:true,你自己加去
另外一个值得说一下的是flush的几个值:
pre组件更新之前调用sync同步执行post组件更新之后执行
三.watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的。
let all = ref("初始数据")
let userReactive = reactive({name: "wsy", age: 1, bro: {name: "wds", age: 2 }})
watch(userReactive,()=>{
all.value = userReactive.age + " 岁"
console.log(all)
})
我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
let all = ref("初始数据")
let userReactive = reactive({name: "wsy", age: 1, bro: {name: "wds", age: 2 }})
watchEffect(()=>{
all.value = userReactive.age + " 岁"
console.log(all)
})
这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 userReactive.age 作为依赖(和计算属性类似)。每当变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 userReactive 作为源值。
对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
四.比较
watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
五.停止
在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {
if (data.value) {
// 数据加载后执行某些操作...
}
})

浙公网安备 33010602011771号