Vue 3 Composition API
Vue 3 的核心 API,通过函数式的方式组织组件逻辑,解决 Options API 在复杂组件中逻辑分散的问题。Composition API 不是替代 Options API,而是提供更灵活的组织方式。
为什么要用 Composition API
Options API 的问题
// Options API —— 一个功能的逻辑分散在不同选项中
export default {
// data 函数返回组件的响应式数据
data() {
return {
count: 0, // 计数器,功能 A 的状态
timer: null // 定时器引用,用于清理
}
},
// computed 计算属性,基于响应式数据自动计算
computed: {
double() {
return this.count * 2 // 依赖 count,当 count 变化时自动更新
}
},
// methods 方法,包含组件的业务逻辑
methods: {
increment() {
this.count++ // 修改响应式数据,触发视图更新
}
},
// mounted 生命周期钩子,组件挂载后执行
mounted() {
// 设置定时器,每秒调用 increment 方法
this.timer = setInterval(this.increment, 1000)
},
// beforeUnmount 生命周期钩子,组件卸载前清理副作用
beforeUnmount() {
clearInterval(this.timer) // 清除定时器,避免内存泄漏
},
// 功能 B 的逻辑也分散在各处...
}Composition API 的优势
<script setup lang="ts">
// 导入组合式函数,每个函数封装一个独立功能
import { useCounter } from './useCounter' // 计数器逻辑
import { useTimer } from './useTimer' // 定时器逻辑
import { useUser } from './useUser' // 用户数据逻辑
// 功能 A 的逻辑集中在一起:计数器 + 定时器
const { count, doubled, increment } = useCounter() // 解构获取计数器状态和方法
const { start, stop } = useTimer(increment, 1000) // 创建定时器,每秒调用 increment
// 功能 B 的逻辑集中在一起:用户数据管理
const { user, fetchUser } = useUser() // 获取用户状态和获取方法
// 生命周期钩子:组件挂载后启动定时器,卸载前停止
onMounted(() => start()) // 启动定时器
onUnmounted(() => stop()) // 停止定时器,避免内存泄漏
</script>ref
大白话解释:ref 就像给数据"包一层盒子"。基本类型(数字、字符串、布尔值)不能直接被 Vue 追踪变化,所以需要用 ref 包起来。访问时用 .value 打开盒子,修改时也用 .value。
为什么用 ref?
- 基本类型需要包装:Vue 无法追踪
let count = 0的变化,但可以追踪ref(0)的变化 - 可以整体替换:
state.value = { name: '李四' }可以替换整个对象 - 模板中自动解包:在
<template>中不需要写.value
ref vs reactive 怎么选?
- 推荐 ref:更灵活,可以替换整个值,TypeScript 类型推断更好
- reactive 适合:表单对象等不需要整体替换的场景
创建基本类型的响应式数据,也支持对象类型。
import { ref } from 'vue' // 从 Vue 导入 ref 函数
// 基本类型:用 ref 包装原始值,使其成为响应式
const count = ref(0) // 创建 ref 对象,初始值为 0
console.log(count.value) // 通过 .value 访问内部值,输出 0
count.value++ // 修改值必须通过 .value
console.log(count.value) // 输出 1
// 对象类型(内部自动调用 reactive,深层属性也是响应式的)
const user = ref({ name: '张三', age: 25 }) // 包装对象
user.value.age++ // 需要 .value 访问内部对象,再直接修改属性
// 数组类型
const list = ref<string[]>([]) // 创建空数组 ref,指定元素类型为 string
list.value.push('item') // 通过 .value 访问数组并添加元素💡 在
<template>中会自动解包,不需要.value:vue<template> <p>{{ count }}</p> <!-- 不需要 count.value,自动解包 --> </template>注意:自动解包仅限顶层 ref。如果 ref 嵌套在对象或数组中,不会自动解包:
vue<template> <!-- ✅ 顶层 ref 自动解包 --> <p>{{ count }}</p> <!-- ❌ 嵌套在对象中的 ref 不会自动解包 --> <p>{{ nested.count }}</p> <!-- 显示 "[object Object]",不是数字 --> <!-- ✅ 需要手动访问 .value --> <p>{{ nested.count.value }}</p> </template>
ref 的类型标注
import { ref, type Ref } from 'vue' // 导入 ref 和 Ref 类型
// 自动推导:TypeScript 根据初始值推断类型
const count = ref(0) // 自动推导为 Ref<number>
// 显式标注:通过泛型参数明确指定类型
const name = ref<string>('张三') // 显式标注为 Ref<string>
const list = ref<number[]>([]) // 显式标注为 Ref<number[]>
// 复杂类型:使用接口定义对象结构
interface User {
id: number // 用户 ID
name: string // 用户名
email?: string // 可选邮箱
}
const user = ref<User | null>(null) // 初始值为 null,类型为 Ref<User | null>
// 函数类型:存储回调函数
const handler = ref<((e: Event) => void) | null>(null) // 事件处理函数ref 解包的细节
const count = ref(0) // 创建基本类型 ref
const obj = ref({ nested: { count: ref(0) } }) // 创建包含 ref 的对象
// 在 reactive 中自动解包:ref 作为 reactive 属性时自动解包
const state = reactive({ count }) // state.count 是 number,不是 ref
state.count++ // 不需要 .value,直接操作
// 在数组中不会自动解包:数组中的 ref 保持原样
const arr = ref([ref(1)]) // 创建包含 ref 的数组
console.log(arr.value[0].value) // 需要两层 .value:外层数组 + 内层 ref
// 在 template 中自动解包(仅限顶层 ref)
// 参考前面的注意事项reactive
大白话解释:reactive 就像给对象"装监控"。对象的每个属性都会被 Vue 监控,任何变化都会被检测到,然后更新页面。
reactive 的限制:
- 不能替换整个对象:
state = { count: 1 }会丢失响应性,因为这是换了一个新对象 - 不能解构:
const { count } = state会丢失响应性,因为解构出来的是普通值
什么时候用 reactive?
- 表单对象(不需要整体替换,只修改单个属性)
- 复杂嵌套对象(深层属性也会被追踪)
创建对象类型的响应式数据,基于 Proxy 实现。
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: { name: '张三', age: 25 },
list: [1, 2, 3],
nested: { deep: { value: 'hello' } },
})
// 直接修改,不需要 .value
state.count++
state.user.name = '李四'
state.list.push(4)
state.nested.deep.value = 'world' // 深层也是响应式的reactive 的限制
// ❌ 不能替换整个对象:let 声明允许重新赋值,但会丢失响应性
let state = reactive({ count: 0 }) // 创建响应式对象
state = reactive({ count: 1 }) // 错误!重新赋值丢失原始引用,之前的绑定失效
// ✅ 用 Object.assign:修改属性而不替换对象
Object.assign(state, { count: 1 }) // 合并新属性到原对象
// ✅ 或用 ref:ref 可以整体替换 .value
const state = ref({ count: 0 }) // 用 ref 包装对象
state.value = { count: 1 } // 正确!替换 .value 保持响应性
// ❌ 不能解构,会丢失响应性
const { count, name } = reactive({ count: 0, name: '张三' })
// count 和 name 是普通值,不是响应式的,修改它们不会触发更新
// ✅ 用 toRefs:将响应式对象的每个属性转换为 ref
import { toRefs } from 'vue'
const state = reactive({ count: 0, name: '张三' }) // 原始响应式对象
const { count, name } = toRefs(state) // 解构出 ref,保持响应性
// count 和 name 是 Ref<number> 和 Ref<string>,需要 .value 访问reactive 的类型标注
// 定义状态接口,描述响应式对象的结构
interface State {
count: number // 计数器
user: User | null // 用户信息,可为空
list: string[] // 字符串列表
}
// 方式一:通过变量类型标注
const state: State = reactive({
count: 0, // 初始值 0
user: null, // 初始无用户
list: [], // 初始空数组
})
// 方式二:通过泛型参数标注(推荐)
const state = reactive<State>({
count: 0, // 初始值 0
user: null, // 初始无用户
list: [], // 初始空数组
})computed
大白话解释:computed 就像"自动计算的公式"。你定义一个计算规则,Vue 会自动算出结果并缓存起来。只有当依赖的数据变化时才会重新计算,没变化就直接返回上次的结果,避免重复运算。
computed 的核心特性:
- 有缓存:依赖不变就不重新计算,比 methods 性能好
- 自动追踪依赖:用到了哪些响应式数据,Vue 自动记录
- 只读 / 可读写:默认只读,也可以设置 setter 实现双向绑定
什么时候用 computed?
- 从已有数据派生新数据(如全名 = 姓 + 名)
- 格式化显示(如价格加 ¥ 符号)
- 条件判断(如表单是否可提交)
- 列表过滤与排序
计算属性,具有缓存特性,只有依赖变化时才重新计算。
import { ref, computed } from 'vue' // 导入 ref 和 computed
// 创建响应式数据
const firstName = ref('张') // 姓
const lastName = ref('三') // 名
const price = ref(100) // 单价
const quantity = ref(3) // 数量
// 只读 computed:依赖变化时自动重新计算,有缓存
const fullName = computed(() => firstName.value + lastName.value) // 全名
const totalPrice = computed(() => price.value * quantity.value) // 总价
// 带缓存的复杂计算:过滤和汇总
const items = ref([
{ name: '苹果', price: 5, count: 2, active: true }, // 激活的商品
{ name: '香蕉', price: 3, count: 5, active: false }, // 未激活的商品
])
// 过滤激活的商品
const activeItems = computed(() => items.value.filter((item) => item.active))
// 计算总金额:单价 × 数量 求和
const totalAmount = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.count, 0)
)
// 可读写 computed:提供 getter 和 setter
const fullNameRW = computed({
// getter:返回全名
get: () => firstName.value + lastName.value,
// setter:拆分字符串更新姓和名
set: (val: string) => {
firstName.value = val[0] // 第一个字符作为姓
lastName.value = val.slice(1) // 剩余字符作为名
},
})
fullNameRW.value = '李四' // 触发 setter,更新 firstName 和 lastNamecomputed vs methods
// ✅ computed —— 有缓存,依赖不变时返回上次结果
const expensive = computed(() => {
console.log('computed 执行了') // 只在依赖变化时执行
return bigList.value.filter(...).map(...).reduce(...) // 复杂计算
})
// ❌ methods —— 每次调用都执行,无缓存
function expensive() {
console.log('methods 执行了') // 每次访问都执行
return bigList.value.filter(...).map(...).reduce(...) // 相同计算
}computed 的常见用法
// 派生状态:从原始数据推导出布尔值
const isLoggedIn = computed(() => !!token.value) // token 存在即登录
const isAdmin = computed(() => roles.value.includes('admin')) // 角色包含 admin
// 格式化显示:将数字格式化为带货币符号的字符串
const formattedPrice = computed(() => `¥${price.value.toFixed(2)}`) // 保留两位小数
// 条件判断:多个条件同时满足才可提交
const canSubmit = computed(() => {
return form.value.username && form.value.password && !loading.value // 用户名、密码非空且未加载中
})
// 列表过滤与排序:根据关键词过滤,按分数降序排列
const filteredList = computed(() => {
return list.value
.filter((item) => item.name.includes(keyword.value)) // 名称包含关键词
.sort((a, b) => b.score - a.score) // 分数高的排前面
})watch / watchEffect
大白话解释:watch 和 watchEffect 都是"数据变化时执行某些操作"的工具,区别在于:
- watch:你明确告诉它"帮我盯着某个数据",数据变了就执行回调(类似保安盯着特定的人)
- watchEffect:你写一段代码,它自动发现里面用到了哪些数据,那些数据一变就重新执行(类似自动追踪器)
什么时候用 watch?
- 需要对比新旧值(如搜索关键词变化时,记录旧值)
- 需要精确控制监听哪个数据
- 需要延迟执行(默认不立即执行)
什么时候用 watchEffect?
- 副作用与多个响应式数据自动绑定(如同时依赖 keyword 和 page)
- 不需要旧值,只关心最新状态
- 需要立即执行一次
watch —— 明确指定侦听源
import { ref, watch, reactive } from 'vue' // 导入 ref、watch、reactive
// 创建响应式数据
const count = ref(0) // 计数器
const keyword = ref('') // 搜索关键词
const user = reactive({ name: '', age: 0 }) // 用户对象
// 侦听单个 ref:当 count 变化时触发回调
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`) // 输出新旧值
})
// 侦听多个源:任一源变化都会触发回调
watch([count, keyword], ([newCount, newKeyword], [oldCount, oldKeyword]) => {
console.log(`count: ${oldCount} → ${newCount}`) // 计数器变化
console.log(`keyword: ${oldKeyword} → ${newKeyword}`) // 关键词变化
})
// 侦听 reactive 对象的某个属性(用 getter 函数)
watch(
() => user.name, // 返回要侦听的属性
(newName, oldName) => {
console.log(`name: ${oldName} → ${newName}`) // 名字变化
}
)
// 侦听 reactive 对象(深度监听,任何嵌套属性变化都触发)
watch(
user, // 直接传入 reactive 对象
(newUser) => {
console.log('user 变化了', newUser) // 对象变化
},
{ deep: true } // 启用深度监听
)
// 立即执行:组件挂载后立即执行一次回调
watch(
keyword,
(newVal) => {
fetchResults(newVal) // 立即搜索当前关键词
},
{ immediate: true } // 立即执行
)watchEffect —— 自动追踪依赖
import { ref, watchEffect } from 'vue' // 导入 ref 和 watchEffect
// 创建响应式数据
const keyword = ref('') // 搜索关键词
const page = ref(1) // 当前页码
// 自动追踪内部用到的所有响应式数据
watchEffect(async () => {
// keyword 或 page 变化时自动执行
const res = await api.search(keyword.value, page.value) // 发起搜索请求
results.value = res.data // 更新结果
})watch vs watchEffect
| 特性 | watch | watchEffect |
|---|---|---|
| 指定侦听源 | 必须 | 不需要,自动追踪 |
| 旧值 | 可获取 | 不可获取 |
| 立即执行 | 需设置 immediate: true | 默认立即执行 |
| 适用场景 | 需要对比新旧值 | 副作用与依赖自动绑定 |
核心区别:
- watch:明确指定侦听源,可以获取新旧值,适合需要精确控制的场景
- watchEffect:自动追踪依赖,立即执行,适合副作用与多个数据绑定的场景
选择建议:
- 需要对比新旧值 → 使用
watch - 依赖多个响应式数据,不需要旧值 → 使用
watchEffect - 需要立即执行一次 → 使用
watchEffect或watch+immediate: true
停止侦听器
// 在 setup 中创建的侦听器,组件卸载时自动停止
// 手动停止:返回停止函数
const stop = watch(count, (newVal) => {
if (newVal > 100) {
stop() // 超过 100 后停止监听,避免不必要的更新
}
})
// 在 onUnmounted 中手动清理(非 setup 顶层创建时)
const unwatch = watchEffect(() => { ... }) // 创建侦听器
onUnmounted(() => unwatch()) // 组件卸载时停止侦听侦听器的 flush 选项
// 默认:回调在 DOM 更新前执行(flush: 'pre')
watch(count, (newVal) => {
// DOM 还没更新,适合准备性操作
})
// flush: 'post' —— DOM 更新后执行,适合操作 DOM
watch(
count,
(newVal) => {
// DOM 已更新,可以安全操作 DOM 元素
},
{ flush: 'post' } // 在 DOM 更新后执行回调
)
// flush: 'sync' —— 同步执行(性能差,少用)
watch(count, (newVal) => { ... }, { flush: 'sync' }) // 同步执行,阻塞更新💡
flush: 'post'和flush: 'sync'是watch/watchEffect的内置选项,无需额外导入。
生命周期钩子
大白话解释: 生命周期钩子就像"人生的各个阶段"。组件从创建到销毁,会经历一系列阶段,你可以在每个阶段插入自己的代码:
- onMounted:组件"出生"了,DOM 已经渲染完毕,可以操作 DOM、发起请求
- onUpdated:组件"更新"了,数据变化导致页面重新渲染
- onBeforeUnmount:组件"临终"前,清理定时器、事件监听等副作用
- onUnmounted:组件"去世"了,已经从页面移除
什么时候用生命周期钩子?
- onMounted:初始化第三方库(如 ECharts)、发起 API 请求、添加全局事件监听
- onBeforeUnmount/onUnmounted:清除定时器、取消事件监听、断开 WebSocket 连接
- onActivated/onDeactivated:KeepAlive 缓存组件的激活/休眠回调
import {
onBeforeMount, // 挂载前
onMounted, // 挂载后
onBeforeUpdate, // 更新前
onUpdated, // 更新后
onBeforeUnmount, // 卸载前
onUnmounted, // 卸载后
onErrorCaptured, // 捕获错误
onActivated, // KeepAlive 激活
onDeactivated, // KeepAlive 停用
onServerPrefetch, // SSR 预取
} from 'vue' // 导入所有生命周期钩子
// 组件挂载后:DOM 已渲染,可以操作 DOM
onMounted(() => {
console.log('组件已挂载,可以操作 DOM')
// 常用:初始化第三方库、发起请求、添加事件监听
})
// 组件卸载前:清理副作用,避免内存泄漏
onBeforeUnmount(() => {
console.log('即将卸载,清理副作用')
// 常用:清除定时器、取消事件监听、断开连接
})
// 组件已卸载:从 DOM 移除
onUnmounted(() => {
console.log('已卸载')
})💡
<script setup>中没有beforeCreate和created,因为 setup 本身就是在这两个钩子之间执行的。
Options API 与 Composition API 生命周期对照
Options API Composition API
─────────────── ──────────────────
beforeCreate → setup()
created → setup()
beforeMount → onBeforeMount()
mounted → onMounted()
beforeUpdate → onBeforeUpdate()
updated → onUpdated()
beforeDestroy → onBeforeUnmount()
destroyed → onUnmounted()
activated → onActivated()
deactivated → onDeactivated()
errorCaptured → onErrorCaptured()provide / inject
大白话解释:provide/inject 就像"家族传承"。祖先组件通过 provide 提供数据,后代组件(不管隔了多少层)通过 inject 直接获取,不需要一层一层传 props。
为什么用 provide/inject?
- 避免 props 逐层传递:爷爷 → 爸爸 → 儿子,如果中间的爸爸不需要这个数据,传 props 就很浪费
- 适合全局配置:主题色、语言、当前用户等全局信息
provide/inject vs Pinia 怎么选?
- provide/inject:组件树内部的局部共享,如表单组件传递表单实例
- Pinia:全局状态管理,需要 DevTools 追踪、持久化、跨页面共享
跨层级组件通信,类似 React 的 Context。
// 祖先组件
import { provide, ref, readonly } from 'vue' // 导入 provide、ref、readonly
// 创建响应式数据
const theme = ref('dark') // 主题色
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark' // 切换主题
}
// 提供只读数据 + 修改方法
provide('theme', readonly(theme)) // 提供只读主题,防止后代直接修改
provide('toggleTheme', toggleTheme) // 提供切换方法
// 提供 Symbol key 避免命名冲突
const ThemeKey = Symbol('theme') // 创建唯一 Symbol
provide(ThemeKey, readonly(theme)) // 使用 Symbol 作为 key// 后代组件
import { inject } from 'vue' // 导入 inject
// 注入数据,第二参数是默认值(祖先未提供时使用)
const theme = inject<string>('theme', 'light') // 注入主题,默认 'light'
const toggleTheme = inject<() => void>('toggleTheme', () => {}) // 注入切换方法
// Symbol key(必须从共享文件导入,确保同一引用)
// import { ThemeKey } from '@/shared/keys'
const theme = inject(ThemeKey) // 使用 Symbol 注入,避免命名冲突组合式函数 (Composables)
大白话解释: Composables 就像"乐高积木"。把可复用的逻辑(如计数器、请求、事件监听)封装成一个个独立的函数,需要时直接"拼装"到组件里。每个 Composable 都是一块积木,可以自由组合。
为什么用 Composables?
- 逻辑复用:多个组件用同一套逻辑,不用重复写
- 关注点分离:把相关逻辑集中在一起,而不是分散在 data/methods/computed 里
- 易于测试:纯函数,单独测试很方便
什么时候用 Composables?
- 多个组件共享同一套逻辑(如表单验证、分页、搜索)
- 封装副作用逻辑(如请求、事件监听、定时器)
- 封装状态管理逻辑(如本地存储、主题切换)
将可复用的逻辑抽离为独立函数,是 Composition API 最强大的模式。
基本结构
// composables/useCounter.ts
import { ref, computed } from 'vue' // 导入 ref 和 computed
// 计数组合式函数:封装计数器逻辑
export function useCounter(initial = 0) {
const count = ref(initial) // 响应式计数器
const doubled = computed(() => count.value * 2) // 双倍值,自动缓存
// 增加计数
function increment() { count.value++ }
// 减少计数
function decrement() { count.value-- }
// 重置为初始值
function reset() { count.value = initial }
// 返回状态和方法
return {
count, // 计数器
doubled, // 双倍值
increment, // 增加方法
decrement, // 减少方法
reset, // 重置方法
}
}<script setup lang="ts">
// 导入组合式函数
import { useCounter } from '@/composables/useCounter'
// 使用组合式函数,解构获取状态和方法
const { count, doubled, increment } = useCounter(10) // 初始值 10
</script>
<template>
<!-- 显示计数器和双倍值 -->
<p>count: {{ count }}, doubled: {{ doubled }}</p>
<!-- 点击按钮增加计数 -->
<button @click="increment">+1</button>
</template>带副作用的 Composable
更多 Composable 示例(useFetch、useEventListener、useLocalStorage 等)详见 自定义指令 & Composables。
// composables/useEventListener.ts
import { onMounted, onUnmounted, type Ref } from 'vue' // 导入生命周期钩子和 Ref 类型
// 事件监听组合式函数:自动管理事件监听器的添加和移除
export function useEventListener(
target: Ref<EventTarget | null> | EventTarget, // 目标元素(ref 或直接元素)
event: string, // 事件名
handler: (e: Event) => void // 事件处理函数
) {
// 组件挂载后添加事件监听
onMounted(() => {
const el = target instanceof EventTarget ? target : target.value // 获取元素
el?.addEventListener(event, handler) // 添加事件监听
})
// 组件卸载前移除事件监听,避免内存泄漏
onUnmounted(() => {
const el = target instanceof EventTarget ? target : target.value // 获取元素
el?.removeEventListener(event, handler) // 移除事件监听
})
}// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue' // 导入 ref、watchEffect、Ref 类型
// 数据请求组合式函数:封装 fetch 请求逻辑
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null) // 响应数据
const error = ref<Error | null>(null) // 错误信息
const loading = ref(false) // 加载状态
// 执行请求
async function execute() {
loading.value = true // 开始加载
error.value = null // 清空错误
try {
const urlValue = typeof url === 'string' ? url : url.value // 获取 URL
const res = await fetch(urlValue) // 发起请求
if (!res.ok) throw new Error(`HTTP ${res.status}`) // 检查响应状态
data.value = await res.json() // 解析 JSON
} catch (e) {
error.value = e as Error // 捕获错误
} finally {
loading.value = false // 结束加载
}
}
// 自动追踪 url 变化,url 变化时自动重新请求
watchEffect(() => { execute() })
return { data, error, loading, execute } // 返回状态和方法
}Vue 3.3+ 新增 API
toValue()
将 ref、getter 或普通值统一解包为原始值,比 unref() 更强大。
import { toValue, ref } from 'vue' // 导入 toValue 和 ref
// toValue 统一处理 ref / getter / 普通值
const count = ref(5) // 创建 ref
toValue(count) // 5,解包 ref
toValue(() => count.value) // 5,调用 getter
toValue(10) // 10,直接返回普通值
// 在 composable 中使用:统一处理不同类型的参数
export function useFetch(url: Ref<string> | string | (() => string)) {
// toValue 统一处理 ref / getter / 普通值
const urlValue = toValue(url) // 获取实际 URL 字符串
}useTemplateRef()(Vue 3.5+)
获取模板 ref 的新推荐方式,替代 ref() + 同名变量。
<script setup lang="ts">
import { useTemplateRef } from 'vue' // 导入 useTemplateRef
// 参数名必须与 template 中的 ref="xxx" 一致
const inputRef = useTemplateRef<HTMLInputElement>('inputRef') // 获取模板 ref
// 组件挂载后自动聚焦输入框
onMounted(() => {
inputRef.value?.focus() // 调用 focus 方法
})
</script>
<template>
<!-- 模板 ref,与 useTemplateRef 参数对应 -->
<input ref="inputRef" />
</template>useId()(Vue 3.5+)
生成唯一的 ID,SSR 安全,适合无障碍属性。
<script setup lang="ts">
import { useId } from 'vue' // 导入 useId
const id = useId() // 生成如 ":rp:" 的唯一 ID,SSR 安全
</script>
<template>
<!-- 使用唯一 ID 关联 label 和 input -->
<label :for="id">用户名</label>
<input :id="id" />
</template>Composables 命名规范
// ✅ 以 use 开头:命名规范,便于识别和搜索
useCounter() // 计数器
useFetch() // 数据请求
useLocalStorage() // 本地存储
useEventListener() // 事件监听
// ✅ 返回值用对象形式(解构时可重命名)
return { count, doubled, increment } // 返回状态和方法
// ✅ 参数用 ref 或 getter(保持响应性)
export function useTitle(newTitle: Ref<string> | (() => string)) { ... } // 接受 ref 或 getter顶层 await
<script setup> 中可以直接使用顶层 await:
<script setup lang="ts">
// 顶层 await:直接使用 await,无需 async 函数包裹
const res = await fetch('/api/user') // 发起请求
const user = await res.json() // 解析 JSON
</script>⚠️ 顶层 await 会将组件变为异步组件,父组件需要
<Suspense>包裹。
内置组件
Teleport(传送门)
大白话解释: Teleport 就像"传送门"。组件的逻辑还在原来的位置,但 DOM 元素可以"传送"到页面的其他位置。最典型的场景是弹窗:弹窗的逻辑写在组件里,但 DOM 渲染到 <body> 下,避免被父组件的 overflow: hidden 或 z-index 影响。
什么时候用 Teleport?
- 弹窗 / 对话框(Modal)
- 抽屉(Drawer)
- 消息提示(Toast / Notification)
- 任何需要脱离父组件 DOM 层级的场景
将组件的 DOM 渲染到指定位置,常用于弹窗、抽屉等需要脱离父组件层级的场景。
<script setup lang="ts">
import { ref } from 'vue' // 导入 ref
const showModal = ref(false) // 控制弹窗显示
</script>
<template>
<!-- 打开弹窗按钮 -->
<button @click="showModal = true">打开弹窗</button>
<!-- 渲染到 body 下,不受父组件样式影响 -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal-content">
<p>弹窗内容</p>
<!-- 关闭弹窗按钮 -->
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</template>💡 Teleport 只改变 DOM 位置,不影响组件逻辑和数据流。
<Teleport to="body">是最常用的目标。
Suspense(异步组件容器)
大白话解释: Suspense 就像"加载等待区"。当组件内部有异步操作(如 await fetch)时,Suspense 会在数据加载完成前显示一个"加载中"的占位内容,加载完毕后再渲染真正的组件。
什么时候用 Suspense?
- 组件内部使用了顶层
await(如<script setup>中直接 await 请求) - 需要统一处理多个异步组件的加载状态
💡 注意:Suspense 在 Vue 3.5 中已稳定,可放心使用。
配合异步组件和顶层 await 使用,在加载完成前显示 fallback 内容。
<!-- AsyncComponent.vue -->
<script setup lang="ts">
// 异步组件:使用顶层 await
const res = await fetch('/api/user') // 发起请求
const user = await res.json() // 解析 JSON
</script>
<template>
<!-- 显示用户名 -->
<p>{{ user.name }}</p>
</template><!-- App.vue -->
<template>
<!-- Suspense 包裹异步组件 -->
<Suspense>
<!-- 默认插槽:异步组件 -->
<template #default>
<AsyncComponent /> <!-- 异步组件 -->
</template>
<!-- fallback 插槽:加载中显示 -->
<template #fallback>
<div>加载中...</div> <!-- 加载占位内容 -->
</template>
</Suspense>
</template>💡 Suspense 在 Vue 3.5 中已稳定,可放心使用。
常见坑点
1. ref 与 reactive 混用
// ❌ reactive 中的 ref 不需要 .value
const count = ref(0) // 创建 ref
const state = reactive({ count }) // ref 作为 reactive 属性时自动解包
// state.count 是 number,不是 ref
// ✅ 但直接访问 count 仍需要 .value
count.value++ // 正确,直接访问 ref
state.count++ // 正确,访问 reactive 属性2. 解构 reactive 丢失响应性
const state = reactive({ count: 0, name: '张三' }) // 创建响应式对象
// ❌ 解构后丢失响应性
const { count, name } = state // count 和 name 是普通值
// ✅ 用 toRefs:将每个属性转换为 ref
const { count, name } = toRefs(state) // count 和 name 是 Ref 类型3. watch 的第一个参数
const state = reactive({ count: 0 }) // 创建响应式对象
// ❌ 不能直接传 reactive 对象的属性
watch(state.count, (newVal) => { ... }) // state.count 是 number,不是响应式引用
// ✅ 用 getter 函数:返回要侦听的属性
watch(() => state.count, (newVal) => { ... }) // getter 函数返回响应式属性4. 组件销毁时清理副作用
// ✅ 在 composable 中自动清理副作用
export function useTimer(callback: () => void, delay: number) {
let timer: number // 定时器引用
// 组件挂载后启动定时器
onMounted(() => {
timer = setInterval(callback, delay) // 设置定时器
})
// 组件卸载前清除定时器,避免内存泄漏
onUnmounted(() => {
clearInterval(timer) // 清除定时器
})
}