侦听器和计算属性类似,都是依赖响应式数据。不过计算属性是在依赖的数据发生变化的时候,重新做二次计算,不会涉及到副作用的操作。而侦听器则刚好相反,在依赖的数据发生变化的时候,允许做一些副作用的操作,例如更改 DOM、发送异步请求…
快速入门
<template>
<div>
<h1>智能机器人</h1>
<div>
<input v-model="question" placeholder="请输入问题" />
</div>
<div v-if="loading">正在加载中...</div>
<div v-else>{{ answer }}</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const question = ref('') // 存储用户输入的问题,以 ? 结束
const answer = ref('') // 存储机器人的回答
const loading = ref(false) // 是否正在加载中
// 侦听器所对应的回调函数,接收两个参数
// 一个是依赖数据的新值,一个是依赖数据的旧值
watch(question, async (newValue) => {
if (newValue.includes('?')) {
loading.value = true
answer.value = '思考中....'
try {
const res = await fetch('https://yesno.wtf/api')
const result = await res.json()
answer.value = result.answer
} catch (err) {
answer.value = '抱歉,我无法回答您的问题'
} finally {
loading.value = false
}
}
})
</script>
<style scoped></style>
在上面的示例中,watch 就是一个侦听器,侦听 question 这个 ref 状态的变化,每次当 ref 状态发生变化的时候,就会重新执行后面的回调函数,回调函数接收两个参数:
- 新的状态值
- 旧的状态值
并且在回调函数中,支持副作用操作。
各种细节
1. 侦听的数据源类型
除了上面快速入门中演示的侦听 ref 类型的数据以外,还支持侦听一些其他类型的数据。
计算属性
<template>
<div>
<input type="text" v-model="firstName" placeholder="first name" />
<input type="text" v-model="lastName" placeholder="last name" />
<p>全名:{{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 设置侦听器
watch(fullName, (newVal, oldVal) => {
console.log(`new: ${newVal}, old: ${oldVal}`)
})
</script>
<style scoped></style>
reactive响应式对象
<template>
<div>
<input type="text" v-model="user.name" placeholder="name" />
<input type="text" v-model="user.age" placeholder="age" />
<p>用户信息:{{ user.name }} - {{ user.age }}</p>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: 'John',
age: 18
})
// 设置侦听器
watch(user, () => {
console.log('触发了侦听器回调函数')
})
</script>
<style scoped></style>
Getter函数
<template>
<div>
<input type="number" v-model="count" />
<p>是否为偶数?{{ isEven() }}</p>
<div>count2: {{ count2 }}</div>
<button @click="count2++">+1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const count2 = ref(0)
// 注意这个函数本身,是每次重新渲染的时候都会重新执行的
function isEven() {
console.log('isEvent 函数被重新执行了')
if (count2.value === 5) {
return 'this is a test'
}
return count.value % 2 === 0
}
// 设置侦听器
// 这里侦听的是函数的返回值结果
// 如果函数返回值发生变化,就会触发侦听器回调函数
watch(isEven, () => {
console.log('触发了侦听器回调函数')
})
</script>
<style scoped></style>
多个数据源所组成的数组
<template>
<div>
<div>
<input type="text" v-model="title" />
</div>
<div>
<textarea v-model="description" cols="30" rows="10"></textarea>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const title = ref('')
const description = ref('')
// 这里侦听的是多个数据源所组成的数组
// 数组里面任何一个数据发生变化,都会触发回调函数
watch([title, description], () => {
console.log('侦听器的回调函数执行了')
})
</script>
<style scoped></style>
2. 侦听层次
这个主要是针对 reactive 响应式对象,当侦听的数据源是 reactvie 类型数据的时候,默认是深层次侦听,这意味着哪怕是嵌套的属性值发生变化,侦听器的回调函数也会重新执行。
<template>
<div>
<h1>任务列表</h1>
<ul>
<li v-for="task in tasks.list" :key="task.id">
{{ task.title }} - {{ task.completed ? '已完成' : '未完成' }}
<button @click="task.completed = !task.completed">切换状态</button>
</li>
</ul>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const tasks = reactive({
list: [
{ id: 1, title: 'Task 1', completed: false },
{ id: 2, title: 'Task 2', completed: true }
]
})
watch(tasks, () => {
console.log('侦听器触发了!')
})
</script>
<style scoped></style>
通过上面的例子,我们可以看出,当侦听的是 reactive 类型的响应式对象时,是深层次侦听的。
虽然上面的这个深层次侦听的特性非常的方便,但是当用于大型数据结构的时候,开销也是很大的,因此一定要留意性能,只在必要的时候再使用。
另外补充一个点,当侦听的是 reactive 对象的时候,不能直接侦听响应式对象的属性值:
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
可以将上面的例子修改为一个 Getter 函数:
const obj = reactive({ count: 0 })
watch(()=>obj.count, (count) => {
console.log(`count is: ${count}`)
})
3. 第三个参数
- 第一个参数:侦听的数据源
- 第二个参数:数据发生变化时要执行的回调函数
- 第三个参数:选项对象
- immediate:true/false
- 默认情况下,watch 对应的回调函数是懒执行的,只有在依赖的数据发生变化时,才会执行回调。
- 但是在某些场景中,我们可能期望立即执行一次,例如请求一些初始化数据,这个时候就可以设置该配置项
- once:true/false
- 侦听器的回调函数只执行一次
- deep:true/false
- 强制转换为深层次侦听器
- 什么时候会用到呢?有些时候使用 watch 来侦听一个由计算属性或者 getter 函数返回的对象的时候,默认就不是深层次的侦听
- 通过设置 deep 可以让这种情况下的对象侦听,也变成深层次的侦听
- immediate:true/false
<template>
<div>
<div v-for="task in tasks" :key="task.id" @click="selectTask(task)">
{{ task.title }} ({{ task.completed ? 'Completed' : 'Pending' }})
</div>
<hr />
<div v-if="selectedTask">
<h3>Edit Task</h3>
<input v-model="selectedTask.title" placeholder="Edit title" />
<label>
<input type="checkbox" v-model="selectedTask.completed" />
Completed
</label>
</div>
</div>
</template>
<script setup>
import { reactive, computed, watch } from 'vue'
const tasks = reactive([
{ id: 1, title: 'Learn Vue', completed: false },
{ id: 2, title: 'Read Documentation', completed: false },
{ id: 3, title: 'Build Something Awesome', completed: false }
])
const selectedId = reactive({ id: null })
// 这是一个计算属性
const selectedTask = computed(() => {
return tasks.find((task) => task.id === selectedId.id)
})
// 侦听的是一个 Getter 函数
// 该 Getter 函数返回计算属性的值
watch(
() => selectedTask.value,
() => {
console.log('Task details changed')
},
{ deep: true }
)
function selectTask(task) {
selectedId.id = task.id
}
</script>
watchEffect
watchEffect 相比 watch 而言,能够自动跟踪回调里面的响应式依赖,对比如下:
watch
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 }
)
watchEffect
// 不再需要显式的指定响应式数据依赖
// 在回调函数中用到了哪个响应式数据,该数据就会成为一个依赖
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
对于只有一个依赖项的场景来讲,watchEffect 的收益不大,但是如果涉及到多个依赖项,那么 watchEffect 的好处就体现出来了。
watchEffect 相比 watch 还有一个特点:如果你需要侦听一个嵌套的数据结构的几个属性,那么 watchEffect 只会侦听回调中用到的属性,而不是递归侦听所有的属性。
<template>
<div>
<h1>团队管理</h1>
<ul>
<li v-for="member in team.members" :key="member.id">
{{ member.name }} - {{ member.role }} - {{ member.status }}
</li>
</ul>
<button @click="updateLeaderStatus">切换领导的状态</button>
<button @click="updateMemberStatus">切换成员的状态</button>
</div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
const team = reactive({
members: [
{ id: 1, name: 'Alice', role: 'Leader', status: 'Active' },
{ id: 2, name: 'Bob', role: 'Member', status: 'Inactive' }
]
})
// 有两个方法,分别是对 Leader 和 Member 进行状态修改
function updateLeaderStatus() {
const leader = team.members.find((me) => me.role === 'Leader')
// 切换状态
leader.status = leader.status === 'Active' ? 'Inactive' : 'Active'
}
function updateMemberStatus() {
const member = team.members.find((member) => member.role === 'Member')
member.status = member.status === 'Active' ? 'Inactive' : 'Active'
}
// 添加一个侦听器
watchEffect(() => {
// 获取到 leader
const leader = team.members.find((me) => me.role === 'Leader')
// 输出 leader 当前的状态
console.log('Leader状态:', leader.status)
})
</script>
<style scoped></style>
回调触发的时机
默认情况下,侦听器回调的执行时机在父组件更新 之后,所属组件的 DOM 更新 之前 被调用。这意味着如果你尝试在回调函数中访问所属组件的 DOM,拿到的是 DOM 更新之前的状态。
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<div v-if="isShow" ref="divRef">
<p>this is a test</p>
</div>
<p>上面的高度为:{{ height }} pixels</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const isShow = ref(false)
const height = ref(0) // 存储高度
const divRef = ref(null) // 获取元素
watch(isShow, () => {
// 获取高度,将高度值给 height
height.value = divRef.value ? divRef.value.offsetHeight : 0
console.log(`当前获取的高度为:${height.value}`)
})
</script>
<style scoped></style>
如果我们期望侦听器的回调在 DOM 更新之后再被调用,那么可以将第三个参数 flush 设置为 post 即可,如下:
watch(
isShow,
() => {
// 获取高度,将高度值给 height
height.value = divRef.value ? divRef.value.offsetHeight : 0
console.log(`当前获取的高度为:${height.value}`)
},
{
flush: 'post'
}
)
停止侦听器
大多数情况下你是不需要关心如何停止侦听器,组件上面所设置的侦听器会在组件被卸载的时候自动停止。
但是上面所说的自动停止仅限于同步设置侦听器的情况,如果是异步设置的侦听器,那么组件被销毁也不会自动停止:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
这种情况下,就需要手动的去停止侦听器。
要手动的停止侦听器,就和 setTimeout 或者 setInterval 类似,调用一下返回的函数即可。
const unwatch = watchEffect(() => {})
// 手动停止
unwatch();
下面是一个具体的示例:
<template>
<div>
<button @click="a++">+1</button>
<p>当前 a 的值为:{{ a }}</p>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const a = ref(1) // 计数器
const message = ref('') // 消息
// 假设我们期望 a 的值到达一定的值之后,停止侦听
const unwatch = watch(
a,
(newVal) => {
// 当值大于 5 的时候,停止侦听
if (newVal > 5) {
unwatch()
}
message.value = `当前 a 的值为:${a.value}`
},
{ immediate: true }
)
</script>
<style scoped></style>