Vue3 组合式 API
大约 10 分钟约 3011 字
Vue3 组合式 API
简介
Vue3 的组合式 API(Composition API)是 Vue3 最重要的新特性。它通过 setup、ref、reactive、computed、watch 等函数组织组件逻辑,解决了 Options API 在大型组件中逻辑分散的问题,提升了代码复用性和可维护性。
特点
基本用法
setup 与 ref/reactive
<!-- UserList.vue -->
<template>
<div>
<h2>用户列表</h2>
<input v-model="keyword" placeholder="搜索用户" />
<button @click="loadUsers">刷新</button>
<ul v-if="!loading">
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }} — {{ user.email }}
</li>
</ul>
<p v-else>加载中...</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const users = ref<User[]>([])
const keyword = ref('')
const loading = ref(false)
// 计算属性
const filteredUsers = computed(() => {
if (!keyword.value) return users.value
const key = keyword.value.toLowerCase()
return users.value.filter(u =>
u.name.toLowerCase().includes(key) ||
u.email.toLowerCase().includes(key)
)
})
// 方法
async function loadUsers() {
loading.value = true
try {
const response = await fetch('/api/users')
const data = await response.json()
users.value = data
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
loadUsers()
})
interface User {
id: number
name: string
email: string
}
</script>reactive — 对象响应式
<script setup lang="ts">
import { reactive } from 'vue'
// reactive 适合复杂对象
const form = reactive({
username: '',
email: '',
password: '',
remember: false
})
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0,
get totalPages() {
return Math.ceil(this.total / this.pageSize)
}
})
function resetForm() {
Object.assign(form, {
username: '',
email: '',
password: '',
remember: false
})
}
</script>watch 与 watchEffect
监听变化
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const userId = ref<number>(1)
const userDetails = ref<User | null>(null)
// watch — 精确监听
watch(userId, async (newId, oldId) => {
console.log(`用户从 ${oldId} 变为 ${newId}`)
const response = await fetch(`/api/users/${newId}`)
userDetails.value = await response.json()
}, { immediate: true }) // 立即执行一次
// watch 多个值
const startDate = ref('')
const endDate = ref('')
watch([startDate, endDate], ([start, end]) => {
if (start && end) {
loadReport(start, end)
}
})
// watchEffect — 自动追踪依赖
const keyword = ref('')
watchEffect(() => {
// 自动追踪 keyword.value
console.log('搜索关键词变化:', keyword.value)
// 适合需要副作用的场景
})
// 深度监听
const settings = reactive({ theme: 'light', language: 'zh' })
watch(settings, (newVal) => {
localStorage.setItem('settings', JSON.stringify(newVal))
}, { deep: true })
</script>Composable — 逻辑复用
提取可复用逻辑
// composables/useUsers.ts
import { ref, computed } from 'vue'
export function useUsers() {
const users = ref<User[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const keyword = ref('')
const filteredUsers = computed(() => {
if (!keyword.value) return users.value
const key = keyword.value.toLowerCase()
return users.value.filter(u =>
u.name.toLowerCase().includes(key)
)
})
async function loadUsers() {
loading.value = true
error.value = null
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (e) {
error.value = (e as Error).message
} finally {
loading.value = false
}
}
async function deleteUser(id: number) {
await fetch(`/api/users/${id}`, { method: 'DELETE' })
users.value = users.value.filter(u => u.id !== id)
}
return {
users,
filteredUsers,
loading,
error,
keyword,
loadUsers,
deleteUser
}
}
interface User {
id: number
name: string
email: string
}<!-- 在组件中使用 -->
<template>
<div>
<input v-model="keyword" placeholder="搜索" />
<button @click="loadUsers">刷新</button>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="user in filteredUsers" :key="user.id">
{{ user.name }}
<button @click="deleteUser(user.id)">删除</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { useUsers } from '@/composables/useUsers'
const {
filteredUsers,
loading,
error,
keyword,
loadUsers,
deleteUser
} = useUsers()
loadUsers()
</script>常用 Composable
// composables/usePagination.ts
import { ref, computed } from 'vue'
export function usePagination(pageSize = 10) {
const page = ref(1)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
function nextPage() {
if (page.value < totalPages.value) page.value++
}
function prevPage() {
if (page.value > 1) page.value--
}
function goToPage(p: number) {
page.value = Math.max(1, Math.min(p, totalPages.value))
}
return { page, total, totalPages, pageSize, nextPage, prevPage, goToPage }
}
// composables/useDebounce.ts
import { ref, watch } from 'vue'
export function useDebounce<T>(value: Ref<T>, delay = 300) {
const debouncedValue = ref(value.value) as Ref<T>
let timeout: ReturnType<typeof setTimeout>
watch(value, (newVal) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
return debouncedValue
}生命周期
组合式 API 生命周期
<script setup lang="ts">
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUnmount
} from 'vue'
onBeforeMount(() => {
console.log('挂载前')
})
onMounted(() => {
console.log('挂载完成')
// DOM 可用,发起请求、添加事件监听等
window.addEventListener('resize', handleResize)
})
onUpdated(() => {
console.log('更新后')
})
onBeforeUnmount(() => {
console.log('卸载前')
})
onUnmounted(() => {
console.log('卸载完成')
// 清理副作用
window.removeEventListener('resize', handleResize)
})
function handleResize() {
console.log('窗口大小变化')
}
</script>进阶 Composable 模式
异步数据获取 — useFetch
// composables/useFetch.ts
import { ref, watch, toValue, type MaybeRef } from 'vue'
interface UseFetchOptions<T> {
immediate?: boolean
initialValue?: T
refetch?: MaybeRef<boolean>
onError?: (err: Error) => void
onSuccess?: (data: T) => void
}
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
loading: Ref<boolean>
execute: () => Promise<void>
}
export function useFetch<T>(
url: MaybeRef<string>,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const { immediate = true, initialValue = null, onError, onSuccess } = options
const data = ref<T | null>(initialValue) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const response = await fetch(toValue(url))
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const result = await response.json()
data.value = result
onSuccess?.(result)
} catch (err) {
error.value = err as Error
onError?.(err as Error)
} finally {
loading.value = false
}
}
if (immediate) execute()
// URL 变化时自动重新请求
watch(() => toValue(url), () => execute())
return { data, error, loading, execute }
}
// 使用示例
const { data: users, loading, error } = useFetch<User[]>('/api/users')表单管理 — useForm
// composables/useForm.ts
import { reactive, computed } from 'vue'
interface ValidationRule {
validator: (value: any) => boolean | string
message: string
}
interface FormField {
value: any
rules: ValidationRule[]
dirty: boolean
}
export function useForm<T extends Record<string, any>>(
initialValues: T,
validationRules: Partial<Record<keyof T, ValidationRule[]>>
) {
const form = reactive(
Object.keys(initialValues).reduce((acc, key) => {
acc[key] = {
value: initialValues[key],
rules: validationRules[key] || [],
dirty: false
}
return acc
}, {} as Record<string, FormField>)
)
const errors = computed(() => {
const result: Record<string, string[]> = {}
for (const [key, field] of Object.entries(form)) {
const fieldErrors: string[] = []
for (const rule of field.rules) {
const result = rule.validator(field.value)
if (result !== true) {
fieldErrors.push(rule.message)
}
}
result[key] = fieldErrors
}
return result
})
const isValid = computed(() => {
return Object.values(errors.value).every(e => e.length === 0)
})
function setFieldValue<K extends keyof T>(key: K, value: T[K]) {
form[key].value = value
form[key].dirty = true
}
function resetForm() {
for (const key of Object.keys(form)) {
form[key].value = initialValues[key]
form[key].dirty = false
}
}
return { form, errors, isValid, setFieldValue, resetForm }
}
// 使用示例
const { form, errors, isValid, resetForm } = useForm(
{ username: '', email: '', password: '' },
{
username: [
{ validator: (v) => v.length >= 3, message: '用户名至少 3 个字符' },
],
email: [
{ validator: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: '邮箱格式不正确' },
],
password: [
{ validator: (v) => v.length >= 8, message: '密码至少 8 个字符' },
],
}
)useEventListener — 自动清理事件监听
// composables/useEventListener.ts
import { onMounted, onUnmounted, type Ref, isRef } from 'vue'
export function useEventListener(
target: Ref<HTMLElement | Window | Document> | HTMLElement | Window | Document,
event: string,
handler: (...args: any[]) => void,
options?: AddEventListenerOptions
) {
onMounted(() => {
const el = isRef(target) ? target.value : target
el.addEventListener(event, handler, options)
})
onUnmounted(() => {
const el = isRef(target) ? target.value : target
el.removeEventListener(event, handler, options)
})
}
// 使用:窗口大小监听
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
useEventListener(window, 'resize', () => {
width.value = window.innerWidth
height.value = window.innerHeight
})useIntersectionObserver — 元素可见性检测
// composables/useIntersectionObserver.ts
import { ref, onMounted, onUnmounted, type Ref } from 'vue'
export function useIntersectionObserver(
target: Ref<HTMLElement | null>,
options?: IntersectionObserverInit
) {
const isVisible = ref(false)
const observer = ref<IntersectionObserver | null>(null)
onMounted(() => {
if (!target.value) return
observer.value = new IntersectionObserver(([entry]) => {
isVisible.value = entry.isIntersecting
}, options)
observer.value.observe(target.value)
})
onUnmounted(() => {
observer.value?.disconnect()
})
return { isVisible }
}
// 使用:懒加载组件
const imageRef = ref<HTMLImageElement | null>(null)
const { isVisible } = useIntersectionObserver(imageRef)ref 与 reactive 深入对比
选择指南与注意事项
// ref 适用场景
// - 基本类型值(string, number, boolean)
// - 需要替换整个值的场景
// - 从组合函数返回的值
// reactive 适用场景
// - 复杂对象/数组
// - 表单数据
// - 不需要替换整个对象的场景
// 常见陷阱
// 1. reactive 不能替换整个对象(会丢失响应式)
const state = reactive({ list: [], count: 0 })
// 错误:state = { list: [1, 2], count: 2 } // 丢失响应式
// 正确:Object.assign(state, { list: [1, 2], count: 2 })
// 2. 解构 reactive 会丢失响应式
const form = reactive({ name: '', email: '' })
// 错误:const { name, email } = form // 丢失响应式
// 正确:使用 toRefs
const { name, email } = toRefs(form) // 保持响应式
// 3. ref 的 .value 在模板中自动解包,在 JS 中需要手动
const count = ref(0)
// 模板:{{ count }} (自动解包)
// JS:count.value++ (需要 .value)
// 4. 嵌套 ref 在 reactive 中自动解包
const state = reactive({
count: ref(0), // 在模板中直接用 state.count
nested: {
value: ref('hello')
}
})computed 与 watch 进阶
计算属性的缓存与副作用
// computed 缓存机制
// - 只在依赖变化时重新计算
// - 多次访问返回缓存值
// - 适合派生状态
// 可写计算属性
const firstName = ref('')
const lastName = ref('')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value: string) => {
const parts = value.split(' ')
firstName.value = parts[0]
lastName.value = parts.slice(1).join(' ')
}
})
// watch 的高级用法
// 1. 监听 getter 函数
const user = reactive({ profile: { name: 'Tom', age: 20 } })
watch(
() => user.profile.age,
(newAge, oldAge) => {
console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
}
)
// 2. 监听多个源并使用不同逻辑
watch(
[() => user.profile.name, () => user.profile.age],
([name, age], [oldName, oldAge]) => {
// 可以分别处理
}
)
// 3. watchPostEffect — DOM 更新后执行
watchPostEffect(() => {
// 在 DOM 更新后执行,可以访问更新后的 DOM
const el = document.getElementById('list')
if (el) {
el.scrollTop = el.scrollHeight
}
})
// 4. watchSyncEffect — 同步执行(谨慎使用)
watchSyncEffect(() => {
// 在响应式数据变化时同步执行,早于 DOM 更新
})依赖注入 provide/inject
跨层级数据传递
// 父组件 provide
// provider.ts
import { inject, provide, type InjectionKey, ref, computed } from 'vue'
// 使用 InjectionKey 提供类型安全
interface UserContext {
user: Ref<User | null>
isLoggedIn: Ref<boolean>
login: (username: string, password: string) => Promise<void>
logout: () => void
}
const UserKey: InjectionKey<UserContext> = Symbol('user')
// 在根组件中 provide
export function provideUserContext() {
const user = ref<User | null>(null)
const isLoggedIn = computed(() => user.value !== null)
async function login(username: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
user.value = await response.json()
}
function logout() {
user.value = null
}
provide(UserKey, { user, isLoggedIn, login, logout })
return { user, isLoggedIn, login, logout }
}
// 在子组件中 inject
export function useUserContext() {
const context = inject(UserKey)
if (!context) {
throw new Error('useUserContext must be used within a UserProvider')
}
return context
}Options API vs Composition API
| 特性 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项分散 | 按功能聚合 |
| 逻辑复用 | Mixins | Composables |
| TypeScript | 需要装饰器 | 原生支持 |
| 代码量 | 模板代码多 | 更简洁 |
| 学习难度 | 低 | 中等 |
| 适用场景 | 简单组件 | 复杂组件 |
优点
缺点
总结
Vue3 组合式 API 是现代 Vue 开发的推荐方式。核心概念:ref/reactive 管理状态,computed 计算派生值,watch 监听变化,composable 提取复用逻辑。简单组件可以继续用 Options API,复杂组件推荐 Composition API。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《Vue3 组合式 API》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《Vue3 组合式 API》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Vue3 组合式 API》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Vue3 组合式 API》最大的收益和代价分别是什么?
