侦听器
什么是侦听器
它之所以叫侦听器,是因为它可以侦听一个或多个响应式数据源数据,并再数据源变化时调用所给的回调函数,就是你传给watch侦听器一个响应式变量,然后当这个变量变化时,自动触发一个你定义的函数,就像一个人被监控了一样,只要这个人一动,摄像头就会报警
这段话来自[Vue3 - watch 侦听器(保姆级详细使用教程)][https://blog.csdn.net/weixin_44166849/article/details/133339827]来讲一下是什么意思
- 侦听一个或多个响应式数据源
- 响应式数据源:就是 Vue3
中用
ref、reactive声明的变量 / 对象(比如const sum = ref(1)、const obj = reactive({name: 'test'})),这类数据的特点是 “变化能被 Vue 感知到”; - 侦听:相当于给这些响应式数据装了
“监控器”,
watch会持续关注这些数据的状态,只要数据有变动,监控器就能立刻察觉。
- 响应式数据源:就是 Vue3
中用
- 数据源变化时调用所给的回调函数
- 你给
watch绑定一个 “处理函数”(回调函数),这个函数不会主动执行,只有当被监控的数据源发生改变时,Vue 才会自动触发这个函数; - 比如你监听
sum = ref(1),点击按钮让sum++(变成 2),此时watch就会立刻执行你写的回调函数,完成你想要的操作(比如打印日志、发请求、更新 DOM 等)。
- 你给
这算是复习了
简单说:watch就是给响应式数据装
“报警器”,数据一变,报警器就触发你预设的 “应对动作”(回调函数)。
来一个对应上述内容的最基本的例子
1 | <template> |
- 页面初始化时,
sum=1,此时回调函数不会执行(因为数据没变化,“人没动”); - 点击按钮,
sum变成 2(“人动了”),watch立刻触发回调函数,控制台打印新值:2 ——— 老值:1(“摄像头报警”); - 再点一次,
sum=3,回调函数再次触发,打印新值:3 ——— 老值:2。
实际上
- 监听器可以不止监听一个数据源,可以同时监听多个响应式数据(比如同时监听
sum和age),只要其中一个变,回调函数就触发; - 不是所有数据都能被监听,必须是
ref/reactive声明的响应式数据,普通变量(比如let sum = 1)变化时,watch感知不到(相当于 “没装摄像头的人,动了也没人知道”); - 惰性特性,默认情况下,
watch不会在页面初始化时执行回调函数,只有数据变化才执行(摄像头不会主动报警,只有人动了才报);如果想初始化就执行,需要加{ immediate: true }配置。
为什么需要watch??
比如你做一个
“搜索框”:用户输入关键词(响应式变量keyword变化),需要自动触发
“发送请求获取搜索结果” 的操作 ——
这时候用watch监听keyword,就能在用户输入时自动执行请求函数,不用手动写点击事件,这就是watch的核心价值:响应数据变化,自动执行副作用操作(发请求、改
DOM、打印日志等)。
Vue3中的侦听器—watch
在 Vue3 中,watch特性进行了一些改变和优化。与 computed
不同,watch通常用于监听数据的变化,并执行一些副作用,例如
发送网络请求、更新DOM等。
Watch的基本用法如下
1 | watch(source, callback, options?) |
其中,source表示要监听的数据,可以是一个响应式的数据对象、一个计算属性或一个方法的返回值;callback表示当数据发生变化时要执行的回调函数;options表示
watch
的一些配置选项,例如immediate、deep、flush等。
其中配置选项代表如下:
immediate:立即执行默认情况下,
watch在初始化时不会执行回调,只有数据变化时才执行。设置immediate: true可以让回调在初始化时立即执行一次。deep:深度监听当监听的是对象或数组时,默认情况下
watch不会监听内部属性的变化(浅监听)。设置deep: true可以开启深度监听。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<template>
<div>
<p>嵌套对象: {{ nestedObj.info.name }}</p>
<button @click="nestedObj.info.name = 'Vue3'">修改嵌套属性</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const nestedObj = reactive({
info: {
name: 'JavaScript',
version: 'ES6'
}
})
// 监听嵌套对象的属性(需要deep)
watch(nestedObj, (newVal) => {
console.log('嵌套对象变化:', newVal)
}, {
deep: true // 深度监听
})
// 或者直接监听嵌套属性(不需要deep)
// watch(() => nestedObj.info.name, (newName) => {
// console.log('名称变化:', newName)
// })
</script>flush:回调执行时机控制回调函数的执行时机,可选值:
'pre'(默认):在 DOM 更新前执行,也就是在数据变化之前执行回调函数'post':在 DOM 更新后执行,也就是在数据变化之后执行回调函数,但是需要等待所有依赖项都更新后才执行'sync':同步执行
那么如何使用 watch 监听不同类型的数据源
监听单个
ref数据源这是最基础的用法,监听用
ref声明的响应式变量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18<template>
<div>
<p>计数器: {{ count }}</p>
<button @click="count++">增加</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 监听单个ref数据
watch(count, (newValue, oldValue) => {
console.log(`计数器从 ${oldValue} 变为 ${newValue}`)
// 这里可以执行副作用操作,比如发送请求、更新DOM等
})
</script>监听多个数据源
可以用数组同时监听多个响应式数据,数组内任何一个数据变化都会触发回调。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<div>
<p>计数器: {{ count }}</p>
<p>用户名: {{ name }}</p>
<button @click="count++">增加计数</button>
<button @click="name = 'Vue3'">修改名称</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('JavaScript')
// 监听多个数据源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`计数变化: ${oldCount} -> ${newCount}`)
console.log(`名称变化: ${oldName} -> ${newName}`)
})
</script>监听
reactive对象监听用
reactive声明的响应式对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<template>
<div>
<p>用户信息: {{ user.name }} - {{ user.age }}</p>
<button @click="user.age++">增加年龄</button>
<button @click="user.name = 'Tom'">修改名字</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: 'Jack',
age: 20
})
// 监听整个reactive对象
watch(user, (newUser, oldUser) => {
console.log('用户信息发生变化:', newUser)
// 注意:reactive对象的oldValue和newValue指向同一个对象
})
</script>监听对象的某个属性
如果只想监听对象的特定属性,需要用函数返回该属性(否则会监听整个对象)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<div>
<p>用户信息: {{ user.name }} - {{ user.age }}</p>
<button @click="user.age++">增加年龄</button>
<button @click="user.name = 'Tom'">修改名字</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const user = reactive({
name: 'Jack',
age: 20
})
// 只监听age属性
watch(() => user.age, (newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
})
</script>
停止监听
在 Vue2 中是没有停止监听的功能,Vue3 之所以提供了停止监听的方法,这是因为持续监听会占用内部资源,很可能咱们某个变量都不需要后续持续监听了,但又没有办法关掉。
在setup中,watch返回一个停止函数,调用它可以停止监听。
1 | <script setup> |
使用watchEffect进行监听
watchEffect是 Vue3
提供的响应式副作用监听,它的核心特点是自动追踪依赖,不需要手动指定监听源,比watch更简洁,是watch的简化版本
- 自动依赖收集:
watchEffect会立即执行一次回调函数,在执行过程中自动追踪用到的响应式数据,当这些数据变化时,回调会重新执行。 - 惰性 vs
立即执行:
watchEffect默认是立即执行的(相当于watch的immediate: true),而watch默认是惰性的。 - 不区分新旧值:
watchEffect的回调没有新旧值参数,只能获取当前最新值。
1 | <template> |
- 初始化时,
watchEffect会立即执行,doubleCount被赋值为 0; - 点击按钮修改
count时,回调自动触发,更新doubleCount。
watchEffect也返回一个停止函数,调用后可停止监听:
1 | <script setup> |
而且watchEffect能够清除副作用,回调函数可接收一个onInvalidate函数,用于清除过期的副作用(如取消网络请求、清除定时器):
1 | <script setup> |
- 每次
keyword变化时,上一次的定时器会被清除,避免无效请求堆积。
同样,watchEffect支持配置选项,通过flush选项控制执行时机,同watch
flush: 'pre'(默认):DOM 更新前执行;flush: 'post':DOM 更新后执行(可获取更新后的 DOM);flush: 'sync':同步执行。
1 | <template> |
使用 computed 替代 watch
在有些情况下,如果我们只是想监听一个值的变化,并在变化时执行一些操作,我们可以使用computed代替watch
computed可以自动缓存计算的结果,只有在它的依赖项变化时才会重新计算,因此可以提高一定的性能。
computed:专注于根据依赖计算新值,有缓存,适合 “推导值” 场景;watch:专注于数据变化时执行副作用(如发请求、改 DOM),无缓存,适合 “响应动作” 场景。
常见的computed替代watch的适用场景如下:
当你只是想根据一个 / 多个数据推导新值,而非执行异步或复杂副作用时,
computed更简洁、性能更好。1
2
3
4
5
6
7<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// computed自动推导值,有缓存,代码更简洁
const doubleCount = computed(() => count.value * 2)
</script>1
2
3
4
5
6
7
8
9
10
11<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const doubleCount = ref(0)
// 用watch做简单计算,冗余且无缓存
watch(count, (newVal) => {
doubleCount.value = newVal * 2
}, { immediate: true })
</script>computed有缓存:只要count不变,多次访问doubleCount.value不会重新计算;watch无缓存:每次触发都会执行回调,若只是计算值会浪费性能。
但是也有computed无法替代watch的场景
当需要执行异步操作或复杂副作用时,必须用
watch(或watchEffect):1
2
3
4
5
6
7
8
9
10
11
12
13
14<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
// 搜索关键词变化时发请求(异步操作),只能用watch
watch(keyword, async (newVal) => {
if (newVal) {
// const res = await fetch(`/api/search?keyword=${newVal}`)
// console.log(res.data)
console.log(`发送搜索请求: ${newVal}`)
}
}, { immediate: true, debounce: 500 }) // 防抖优化
</script>computed必须同步返回值,无法处理异步逻辑;watch适合处理这类需要 “响应变化并执行动作” 的场景。
模板引用
什么是模板引用
虽然 Vue 提倡声明式开发(不直接操作 DOM),但在实际项目中,我们总会遇到不得不直接访问 DOM 或子组件实例的场景。
在 Vue 中,我们通常通过改变数据(State)来让页面自动更新。但在以下场景中,数据驱动无法满足需求,我们需要直接操作 DOM:
- 管理焦点:页面加载后自动聚焦到输入框。
- 媒体播放:控制
<video>或<audio>的play() / pause()。 - 集成第三方库:初始化 ECharts、D3.js、Mapbox 等需要挂载到具体 DOM 节点上的库。
- 获取元素尺寸/位置:做动画或拖拽时,需要
getBoundingClientRect()。
像这样的常见的还有很多,也就是说,模板引用是 Vue 提供的一种方式,让你能在代码中直接获取到页面上的 DOM 元素或组件实例
模板引用就是 Vue 留给我们的 “后门”,让我们在必要时能优雅地操作 DOM / 组件,而不是完全禁止这种操作。
可以把模板引用想象成
- 你家里有很多家具(DOM 元素),你给沙发贴了个标签写着 “mySofa”,给电视贴了 “myTV”;
- 之后你想搬动沙发或打开电视时,直接喊标签名就能找到对应的家具,不用满屋找。
在 Vue 里,模板引用就是给 DOM 元素 / 组件贴的 “标签”,让你能在代码里精准找到并操作它们
如何使用模板引用
如何使用模板引用呢?
- 第一步:在模板中的 DOM 元素 / 组件上添加
ref属性(贴标签); - 第二步:在代码中用
ref()函数创建同名的响应式变量,Vue 会自动把 DOM 元素 / 组件实例赋值给这个变量。
所以,我们就可以这样操作DOM元素,写一个聚焦的输入框为例子
1 | <template> |
- 页面加载后,输入框会自动获得焦点,这就是通过模板引用直接操作 DOM 的效果;
- 注意:必须在
onMounted钩子中操作(DOM 渲染完成后),否则inputRef.value是null。
如何获取组件实例?
如果ref加在子组件上,能获取到子组件的实例(方便调用子组件的方法):
1 | <!-- 子组件 Child.vue--> |
1 | <!-- 父组件 Child.vue--> |
Vue 3.5 引入了
useTemplateRef,这使得引用的定义更加语义化,且与数据 ref
区分开来。
例如,点击按钮复制文本框内容就可以这么写
1 | <script setup> |
像我们上面的例子,我们都是建一个与模板 ref
同名的响应式变量,并初始化为
null。使用useTemplateRef就可以直接创建模板引用的变量
生命周期与安全访问
模板引用在组件挂载(Mount)前是 null。
所以你不能在<script setup>
的顶层直接访问它,因为那时 DOM 还没生成,才到创建这步
1 | import { onMounted, watchEffect } from 'vue' |
组件上的 Ref
当 ref 用在组件标签上时,你拿到的是组件实例。
什么意思?也就是,当把ref属性加在子组件标签上时,父组件通过这个ref拿到的不是
DOM 元素,而是子组件的实例对象。
而且用<script setup>写的子组件,内部的方法、变量默认是
“藏起来” 的(私有),父组件就算拿到子组件实例,也看不到这些内容。
所以这时候就可以使用defineExpose,这个在前面说了,它就是把子组件想让父组件访问的内容暴露出去,这样父组件才能调用这些子组件中被暴露的内容。
例如,父组件控制子组件的弹窗打开
1 | <!--子组件 (ChildModal.vue)--> |
1 | <!--父组件 (Parent.vue)--> |
v-for 中的模板引用
当 ref 用在循环里时,引用的值会变成一个数组。
注意,数组中元素的顺序不保证与源数据数组的顺序一致(通常取决于更新的时机)。
例如,我们经常需要制作图片画廊的入场动画作为头图展示
1 | <script setup> |
函数模板引用
函数模板引用是什么?
普通的模板引用是绑定一个字符串(如ref="input"),Vue
会自动帮你把对应的 DOM 元素赋值给同名的ref变量;
而函数模板引用是绑定一个函数(如:ref="(el) => { ... }"),当
DOM 元素被创建或销毁时,Vue 会自动调用这个函数,并把当前 DOM
元素(el)作为参数传给你。
Vue 在 DOM 元素 “出生” 或 “消失” 时,主动通知你,并把元素本身交给你处理,你可以自定义这个存储或操作的逻辑。(使用模板引用中的函数)
例如,我们使用模板函数引用,解决上面的在v-for循环中,如果用普通字符串ref,会的遇到两个问题:
- 无法区分元素:循环生成的多个元素会共享同一个
ref名称,最终只能拿到最后一个元素; - 顺序不可靠:如果数组顺序变化(如排序、筛选),
ref对应的 DOM 元素顺序也会乱,无法通过索引准确找到目标元素。
而函数模板引用可以让你用数据的唯一标识(如 id)来关联 DOM 元素,彻底解决这些问题
准备数据和存储容器
1 | const list = [ |
定义函数模板引用的处理函数
1 | const setItemRef = (el, id) => { |
- 这个函数会被 Vue 自动调用,参数规则:
- 当 DOM 元素被渲染到页面时,
el是当前 DOM 元素,id是我们传入的项的 id; - 当 DOM
元素被移除时(如数组项被删除),
el会是null,此时需要清理 Map 中的无效数据。
- 当 DOM 元素被渲染到页面时,
模板中绑定函数模板引用
1 | <li v-for="item in list" :key="item.id"> |
使用存储的 DOM 元素(滚动到指定项)
1 | const scrollToItem = (id) => { |
- 点击按钮时,传入对应项的
id,从itemMap中取出 DOM 元素,调用scrollIntoView方法实现滚动,无论数组顺序如何变化,都能精准找到目标元素。
那么整个的执行流程就是这样的
- 页面初始化渲染:
- 循环
list生成两个<span>元素; - Vue
为每个
<span>调用:ref绑定的函数,把el(<span>DOM)和item.id(101/102)传给setItemRef; itemMap中存入101 → <span>A</span>、102 → <span>B</span>。
- 循环
- 点击 “滚动到我” 按钮:
- 调用
scrollToItem(item.id),传入对应 id(如 101); - 从
itemMap中取出 id 为 101 的<span>元素; - 调用
scrollIntoView,页面滚动到该元素位置。
- 调用
- 如果数组变化(如删除 id=102 的项):
- 对应的
<span>被销毁,Vue 会再次调用函数,传入el=null和id=102; setItemRef执行itemMap.delete(102),清理无效数据。
- 对应的
混入
什么是混入
混入(Mixins)是 Vue
中一种代码复用机制,本质是一个包含组件选项(如data、methods、生命周期钩子等)的对象,它可以包含任何组件选项。当组件使用混入对象时,混入对象的所有选项会被
“合并”
到组件自身的选项中,从而实现逻辑复用。这意味着组件可以接收混入对象的方法、生命周期钩子、计算属性等。
也就是说,在 Vue 3 中,混入(Mixins)是一种将一组组件选项合并到目标组件中的技术。通过混入,开发者可以在多个组件之间共享重复的逻辑、方法和数据,从而提高代码的可复用性和可维护性。
在 Vue 3 中,混入(Mixins)仍然是一种用于代码复用的模式,但它并不是
Vue 3 官方推荐的最佳实践,特别是在引入了
Composition API(也称hooks)之后。混入在 Vue 2
中被广泛使用,但在 Vue 3 中,官方更推荐使用组合式 API(Composition
API)来实现类似的功能,因为组合式 API
提供了更好的逻辑关注点分离和可维护性。
使用混入对象
首先创建一个混入对象,包含需要复用的选项:
1 | // mixins/logMixin.js |
在组件中通过mixins选项引入混入对象:
1 | <template> |
- 组件挂载时,先执行混入的
mounted(打印[Log 1]: 组件已挂载),再执行组件自身的mounted(打印[Log 2]: 组件自身的挂载钩子); - 点击按钮时,调用混入对象
logMixin的printLog方法,打印[Log 3]: 按钮被点击。
Vue3中代替混入的Hook实现
到底说, Vue 3 的官方文档和社区都鼓励使用 Composition API 来替代 mixins,因为 Composition API 提供了更好的代码组织和复用能力。所以混入只是一个了解性的内容。
我们都知道 Vue3 中有很多钩子函数
Composition API 主要包括以下几种类型的 hooks:
- 逻辑钩子 (Logical Hooks):
setup():是 Composition API 的入口点,用于定义和组织组件的逻辑。onBeforeMount(), onMounted(), onBeforeUpdate(), onUpdated(), onBeforeUnmount(), onUnmounted():这些生命周期钩子允许你在组件的不同生命周期阶段运行代码。onActivated(), onDeactivated():用于keep-alive组件的激活/非激活状态。onErrorCaptured():用于捕获组件树中的错误。
- 响应式数据钩子 (Reactive Data Hooks)
ref():创建一个响应式的引用对象。reactive():为对象创建响应式的副本。computed():创建计算属性。watch(),watchEffect():自动收集响应式数据的依赖关系并执行副作用。
- 上下文钩子(Context Hooks):
useContext():访问组件树中的上下文。
- 其他钩子
useProvide(), useInject():提供和注入服务,类似于Vue的provide/inject机制。useRouter(), useRoute():在Vue Router中使用,分别获取路由器实例和当前路由对象。useStore():在Vuex中使用,获取store实例
钩子也是可以自定义的,代替混入的形式
我们来封装一个useRequest钩子,用于处理 API
请求、加载状态、错误处理等通用逻辑,替代混入的方式。
1 | // 封装通用的请求逻辑Hook |
组件中通过import { useRequest } from './hooks/useRequest.js'明确引入,一眼就能知道data、loading等状态来自哪里,而混入的逻辑是
“隐式” 的。
然后组件中使用这个 Hook:UserList.vue
1 | <template> |
钩子肯定是可以被复用的,这样就替代了混入的形式,另一个组件复用同一个
Hook:ArticleDetail.vue
1 | <template> |
第二个组件中fetchArticle(2)可以传递参数给请求函数,混入很难做到这种灵活的参数控制。
而且很明显,如果一个组件需要同时处理多个请求,只需多次调用 Hook 并解构不同名称:
1 | const { execute: fetchUsers } = useRequest(fetchUsersApi) |
而混入如果有同名状态 / 方法,会直接冲突覆盖。
hook 是比 混入 要好很多的
响应式工具
什么是响应式工具
ref()
和reactive()
这俩前面都说过了,但是我没说这俩是一种响应式工具罢了,但是一直以来其实也是当成一个类似工具的方法来用
ref() -
创建值类型响应式数据,ref用于将基本数据类型(如数字、字符串、布尔值)包装成响应式对象,也可以用于包装对象(但内部会自动用reactive处理)。
reactive用于将对象 /
数组转换为响应式代理对象,适合处理复杂数据结构。
复习一下
computed()
computed用于根据响应式数据推导新值,具有缓存特性(依赖不变时不会重新计算)。
这个大家也比较熟悉了
1 | <script setup> |
- 缓存结果,依赖不变时复用计算值;
- 支持只读和可写两种模式;
- 适合处理数据转换 / 组合场景。
readonly()
它用于创建只读响应式数据,防止数据被修改。
1 | <script setup> |
- 只读代理会跟随原数据的变化而更新;
- 适合保护数据不被意外修改(如传入子组件的 props)。
shallowRef() &
shallowReactive()
这两个响应式工具是浅响应式的
与ref/reactive的深度响应式不同,这两个工具只监听第一层属性的变化,适合处理大型数据结构(优化性能)。
1 | <script setup> |
这种在大型不可变数据,像后端返回的海量分页或者海量列表的时候作为快速加载很有用
triggerRef()
浅响应式可能会带来没法及时更新或者响应的问题,triggerRef()它用于用于强制触发shallowRef的更新,即使只修改了内部属性。
1 | <script setup> |
toRef() &
toRefs()
保留响应式引用工具
toRef:为响应式对象的某个属性创建 ref,保留响应式关联;toRefs:将响应式对象的所有属性转换为 ref,常用于解构响应式对象。
这个相对来说不太好明白
首先要明白:直接解构reactive创建的响应式对象,会导致解构出的属性失去响应式。因为直接解构reactive对象得到的是普通值,而且这些值是常量(const声明),根本无法修改,更别说触发响应式更新了。
1 | <script setup> |
toRefs()的作用就是把reactive对象的每个属性都转换成独立的
ref,这样解构后每个属性依然是响应式的。
toRef()就是reactive对象中的单个属性
1 | <script setup> |
- 转换后的 ref 与原对象保持响应式关联;
- 适合解构响应式对象时保留响应式(避免解构后失去响应式)。
原理:toRef()/toRefs()是 “引用” 不是
“拷贝”
- 可以把它们理解为:给原响应式对象的属性
“贴了一个标签”,通过标签操作属性,本质还是在操作原对象。
- 拷贝:比如
const name = user.name,是把值复制一份,和原对象无关; - 引用:比如
const nameRef = toRef(user, 'name'),是指向原对象的name属性,和原对象共享数据。
- 拷贝:比如
unref()
这个工具自动解包 ref
unref本身也是一个语法糖:如果参数是
ref,返回.value;否则返回参数本身。
1 | import { ref, unref } from 'vue' |





