文章目录
- 前言
- [一、 watch 监视的 5 种情况](#一、 watch 监视的 5 种情况)
-
- [情况 1:监视 ref 定义的【基本类型】数据](#情况 1:监视 ref 定义的【基本类型】数据)
- [情况 2:监视 ref 定义的【对象类型】数据](#情况 2:监视 ref 定义的【对象类型】数据)
- [情况 3:监视 reactive 定义的【对象类型】数据](#情况 3:监视 reactive 定义的【对象类型】数据)
- [情况 4:监视 ref 或 reactive 定义的对象中的【某个属性】](#情况 4:监视 ref 或 reactive 定义的对象中的【某个属性】)
- [情况 5:监视上述多个数据源的【组合】](#情况 5:监视上述多个数据源的【组合】)
- [二、 watchEffect 高级监听器](#二、 watchEffect 高级监听器)
-
- [2.1 什么是 watchEffect?](#2.1 什么是 watchEffect?)
- [2.2 watch 与 watchEffect 的对比](#2.2 watch 与 watchEffect 的对比)
- [2.3 代码示例](#2.3 代码示例)
- [三、 标签的 ref 属性](#三、 标签的 ref 属性)
-
- [3.1 用在普通 HTML DOM 标签上](#3.1 用在普通 HTML DOM 标签上)
- [3.2 用在组件标签(Component)上](#3.2 用在组件标签(Component)上)
- [四、 父子组件传值(父传子:Props)](#四、 父子组件传值(父传子:Props))
-
- [4.1 父组件:通过属性绑定传递数据](#4.1 父组件:通过属性绑定传递数据)
- [4.2 子组件:使用 `defineProps` 接收数据](#4.2 子组件:使用
defineProps接收数据)
- [五、 生命周期函数(Lifecycle Hooks)](#五、 生命周期函数(Lifecycle Hooks))
-
- [5.1 概念](#5.1 概念)
- [5.2 Vue 2 选项式与 Vue 3 组合式 API 对照](#5.2 Vue 2 选项式与 Vue 3 组合式 API 对照)
- [5.3 组合式 API 生命周期用法示例](#5.3 组合式 API 生命周期用法示例)
- [六、 自定义 Hook (逻辑复用)](#六、 自定义 Hook (逻辑复用))
-
- [6.1 什么是 hook?](#6.1 什么是 hook?)
- [6.2 规范与设计](#6.2 规范与设计)
- [6.3 简单实战:封装一个管理累加功能的 Hook](#6.3 简单实战:封装一个管理累加功能的 Hook)
- 结语
前言
Vue 3 组合式 API 让组件逻辑更加清晰可复用。本文从 watch 五种监视场景到父子组件传值,再到生命周期与自定义 Hook,系统梳理实际开发中最常用的核心技巧。
一、 watch 监视的 5 种情况
在 Vue 3 中,watch 用于监视数据的变化 。watch 只能监视以下四种数据源:ref 定义的数据、reactive 定义的数据、一个函数(getter)、或者包含上述内容的数组 。在实际开发中通常分为以下 5 种使用场景 :
情况 1:监视 ref 定义的【基本类型】数据
特点 :直接写数据名即可,监视的是其 .value 值的改变 。
js
<script lang="ts" setup>
import { ref, watch } from 'vue'
let sum = ref(0)
function changeSum() {
sum.value += 1
}
// 监视基本类型:无需加 .value
const stopWatch = watch(sum, (newValue, oldValue) => {
console.log('sum变化了', newValue, oldValue)
if (newValue >= 10) {
stopWatch() // 显式调用可停止监视
}
})
</script>
情况 2:监视 ref 定义的【对象类型】数据
特点 :直接写数据名,默认监视的是对象的地址值 。若想监视对象内部数据的变化,需要手动开启 deep: true(深度监视) 。
-
注意:
- 若修改的是对象内部的属性,
newValue和oldValue是完全相同的(因为它们指向内存中的同一个对象) 。 - 若修改了整个对象(替换了地址),
newValue为新对象,oldValue为旧对象 。
- 若修改的是对象内部的属性,
js
<script lang="ts" setup>
import { ref, watch } from 'vue'
let person = ref({ name: '张三', age: 18 })
// 开启深度监视,内部属性变化也能检测到
watch(person, (newValue, oldValue) => {
console.log('person变化了', newValue, oldValue)
}, { deep: true })
</script>
情况 3:监视 reactive 定义的【对象类型】数据
特点 :监视 reactive 定义的对象时,默认自动开启了深度监视 ,并且配置 deep: false 是无效的 。同样,回调中的 newValue 和 oldValue 指向同一个对象,二者相等 。
代码段
js
<script lang="ts" setup>
import { reactive, watch } from 'vue'
let person = reactive({ name: '张三', age: 18 })
// 默认开启深度监视
watch(person, (newValue, oldValue) => {
console.log('reactive person变化了', newValue, oldValue) [cite: 25]
})
</script>
情况 4:监视 ref 或 reactive 定义的对象中的【某个属性】
特点 :要监视对象里的某一个特定属性,最好写成 Getter 函数形式(即一个返回值的箭头函数) 。
- 若该属性值是基本类型:必须写成函数形式 。
- 若该属性值是对象类型 :可以直接写,也可以写成函数,更推荐写成函数 。若写成函数式且关注该属性内部的变化,需要手动开启
{ deep: true }。
js
<script lang="ts" setup>
import { reactive, watch } from 'vue'
let person = reactive({
name: '张三',
car: { c1: '奔驰', c2: '宝马' } [cite: 25]
})
// 1. 监视基本类型属性:必须写成函数式
watch(() => person.name, (newValue, oldValue) => {
console.log('person.name变化了', newValue, oldValue) [cite: 25]
})
// 2. 监视对象类型属性:写成函数式并手动开启深度监视
watch(() => person.car, (newValue, oldValue) => {
console.log('person.car变了', newValue, oldValue) [cite: 26]
}, { deep: true }) [cite: 26]
</script>
情况 5:监视上述多个数据源的【组合】
特点 :第一个参数传入一个数组,数组中包含你想同时监视的数据源 。
js
<script lang="ts" setup>
import { reactive, watch } from 'vue'
let person = reactive({ name: '张三', car: { c1: '奔驰' } }) [cite: 25, 27]
// 同时监视名字和车子的对象 [cite: 26]
watch([() => person.name, person.car], (newValue, oldValue) => {
console.log('数据源组合发生了变化', newValue, oldValue) [cite: 26]
}, { deep: true }) [cite: 26]
</script>
二、 watchEffect 高级监听器
2.1 什么是 watchEffect?
watchEffect 会立即运行一个函数,同时响应式地追踪其内部使用到的依赖(属性),并在依赖更改时自动重新执行该函数 。
2.2 watch 与 watchEffect 的对比
-
相同点:都能监听响应式数据的变化并执行逻辑 。
-
不同点:
-
watch:必须明确指出要监视的每一个数据源 。 -
watchEffect:不用明确指出监视的数据 。它的函数里用到哪些属性,Vue 就会自动追踪并监视哪些属性,非常智能和简练 。
-
2.3 代码示例
js
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
let temp = ref(0)
let height = ref(0)
// 自动追踪:函数内部用到了 temp.value 和 height.value,所以这两个变量变化时都会触发该函数 [cite: 27, 28]
watchEffect(() => {
if (temp.value >= 50 || height.value >= 20) {
console.log('水温达到50℃,或水位达到20cm,立刻联系服务器!')
}
})
</script>
三、 标签的 ref 属性
在 Vue 3 中,标签上的 ref 属性用于注册模板引用 。
3.1 用在普通 HTML DOM 标签上
- 作用 :获取的是对应的真实 DOM 节点 ,用于替代传统的
document.getElementById。
js
<template>
<h1 ref="title">前端开发</h1> [cite: 29]
<button @click="showLog">打印节点</button> [cite: 29]
</template>
<script lang="ts" setup>
import { ref } from 'vue'
// 2. 声明一个名字和 template 中 ref 完全一致的变量
let title = ref() [cite: 29]
function showLog() {
// 3. 在挂载后通过 .value 拿到真正的 DOM 节点 [cite: 29]
console.log(title.value) [cite: 29]
}
</script>
3.2 用在组件标签(Component)上
-
作用 :获取的是该组件的实例对象 。
-
安全性限制(核心注意点) :在 Vue 3 的
<script setup>模式下,子组件内部的数据默认是封闭(私有)的 。父组件即使通过ref拿到了子组件实例,默认也无法读取子组件的数据或方法 。 -
解决方案 :子组件必须使用
defineExpose宏显式地把允许外部访问的内容暴露出去 。
js
<template>
<Person ref="ren"/> [cite: 29]
<button @click="test">测试读取</button> [cite: 29]
</template>
<script lang="ts" setup>
import Person from './components/Person.vue' [cite: 29]
import { ref } from 'vue'
let ren = ref() [cite: 29]
function test() {
console.log(ren.value.name) // 成功拿到:张三
console.log(ren.value.age) // 成功拿到:18
}
</script>
js
<script lang="ts" setup>
import { ref, defineExpose } from 'vue'
let name = ref('张三')
let age = ref(18)
// 必须通过 defineExpose 显式暴露给父组件
defineExpose({ name, age })
</script>
四、 父子组件传值(父传子:Props)
Props 是实现父组件向子组件传递数据的最基础、最标准的手段 。
4.1 父组件:通过属性绑定传递数据
js
<template>
<Person :list="persons"/>
</template>
<script lang="ts" setup>
import Person from './components/Person.vue'
import { reactive } from 'vue'
let persons = reactive([
{ id: '01', name: '张三', age: 18 },
{ id: '02', name: '李四', age: 19 }
])
</script>
4.2 子组件:使用 defineProps 接收数据
defineProps 是 Vue 3 的编译器宏,不需要单独 import 引入 。它支持三种常见的接收写法 :
js
<script lang="ts" setup>
import { defineProps } from 'vue' // 可省,不引入也能直接用
// 写法一:仅接收(通过数组形式接收)
// const props = defineProps(['list'])
// 写法二:接收 + 限制类型(使用 TypeScript 泛型限制)
// defineProps<{ list: any[] }>()
// 写法三:接收 + 限制类型 + 指定默认值(使用 withDefaults 宏,生产推荐)
let props = withDefaults(defineProps<{ list?: any[] }>(), {
list: () => [{ id: 'default01', name: '小猪佩奇', age: 5 }]
})
console.log(props.list) // 在 JS/TS 中需要通过 props.xxx 访问
</script>
<template>
<div>
<li v-for="item in list" :key="item.id">
{{ item.name }} -- {{ item.age }}
</li>
</div>
</template>
五、 生命周期函数(Lifecycle Hooks)
5.1 概念
Vue 组件实例在创建到销毁的过程中,会在特定的时机自动调用一些特定的函数,这些函数统称为生命周期钩子 。
5.2 Vue 2 选项式与 Vue 3 组合式 API 对照
组件生命周期整体分为四个阶段:创建、挂载、更新、卸载/销毁,每个阶段一前一后各有两个钩子 。
| 阶段 | Vue 2 选项式 API | Vue 3 组合式 API (导入使用) | 说明与核心作用 |
|---|---|---|---|
| 创建 | beforeCreate / created |
不需要专门的钩子 | 直接写在 setup 脚本中即可(领先于所有钩子执行) 。 |
| 挂载 | beforeMount |
onBeforeMount |
模板编译完毕,即将挂载到页面 。 |
| 挂载 | mounted |
onMounted |
极其常用! 页面真实的 DOM 挂载完毕 。通常用于发送网络请求、初始化图表、绑定外部原生 DOM 事件。 |
| 更新 | beforeUpdate |
onBeforeUpdate |
响应式数据发生了改变,但页面尚未开始重新渲染。 |
| 更新 | updated |
onUpdated |
数据改变导致的页面 DOM 重新渲染已完成 。 |
| 卸载 | beforeDestroy |
onBeforeUnmount |
极其常用! 组件即将被卸载、销毁之前 。用于清除定时器、解绑全局自定义事件,防止内存泄漏。 |
| 卸载 | destroyed |
onUnmounted |
组件已经彻底卸载 。 |
5.3 组合式 API 生命周期用法示例
js
<script lang="ts" setup>
import { onMounted, onUpdated, onBeforeUnmount } from 'vue'
// 挂载完毕
onMounted(() => {
console.log('组件挂载成功了,可以发请求了!')
})
// 卸载之前
onBeforeUnmount(() => {
console.log('组件即将销毁,正在做收尾和清理定时器的工作...')
})
</script>
六、 自定义 Hook (逻辑复用)
6.1 什么是 hook?
Vue 3 中的 hook 本质是一个普通的函数 。它把 setup 函数中使用的组合式 API(比如 ref、reactive、computed、生命周期钩子等)进行了高度分块封装 。
6.2 规范与设计
- 模块文件通常存放在项目的
src/hooks/目录下 。 - 文件名和函数名规范以
use开头(例如:useSum.ts、useMouse.ts)。 - 在 Hook 内部定义独立的响应式数据和业务方法,并最终将外部需要用到的数据、方法通过
return暴露出来。
6.3 简单实战:封装一个管理累加功能的 Hook
- 步骤 1:抽取封装逻辑 (
src/hooks/useSum.ts)
typescript
import { ref, onMounted } from 'vue'
export default function useSum() {
// 1. Hook 内部的响应式数据
let sum = ref(0)
// 2. Hook 内部的方法
function add() {
sum.value += 1
}
// 3. 也可以配合生命周期钩子
onMounted(() => {
console.log('useSum hook 伴随组件挂载而执行')
})
// 4. 向外暴露组件需要的东西
return { sum, add }
}
- 步骤 2:在任意组件中优雅复用 (
Person.vue)
js
<template>
<div class="person">
<h2>当前求和为:{{ sum }}</h2>
<button @click="add">点我加 1</button>
</div>
</template>
<script lang="ts" setup>
// 引入自定义的 hook
import useSum from '@/hooks/useSum'
// 调用 hook 并解构出里面的响应式数据和方法,逻辑极度清晰且高内聚
const { sum, add } = useSum()
</script>
结语
掌握 watch 与 watchEffect 的差异、defineExpose 的显式暴露机制,以及自定义 Hook 的封装规范,是写出高内聚、低耦合 Vue 3 组件的关键。组合式 API 的精髓在于让逻辑回归功能本身。