自 Vue 3.0 发布以来,组合式 API 已经从"新特性"演变为 Vue 的默认开发范式。它解决了 Options API 在大型组件中逻辑分散、难以复用的核心痛点,同时保留了 Vue 一贯的优雅和直观。
ref 与 reactive:如何选择?
这是新手最常问的问题。本质上,reactive 适合对象型状态,ref 适合原始类型与"需要整体替换"的对象。
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
export interface Product {
id: number
name: string
price: number
category: string
tags: string[]
rating: number
inStock: boolean
}
export interface FilterOptions {
keyword: string
categories: string[]
priceRange: [number, number]
minRating: number
inStockOnly: boolean
}
export function useProductFilter(initialProducts: Product[]) {
const products = ref<Product[]>(initialProducts)
const loading = ref(false)
const error = ref<string | null>(null)
const filters = reactive<FilterOptions>({
keyword: '',
categories: [],
priceRange: [0, 10000],
minRating: 0,
inStockOnly: false
})
const filteredProducts = computed(() => {
let result = [...products.value]
if (filters.keyword) {
const kw = filters.keyword.toLowerCase()
result = result.filter(p => p.name.toLowerCase().includes(kw))
}
if (filters.categories.length > 0) {
result = result.filter(p => filters.categories.includes(p.category))
}
result = result.filter(p =>
p.price >= filters.priceRange[0] &&
p.price <= filters.priceRange[1]
)
if (filters.minRating > 0) {
result = result.filter(p => p.rating >= filters.minRating)
}
if (filters.inStockOnly) {
result = result.filter(p => p.inStock)
}
return result
})
const stats = computed(() => ({
total: filteredProducts.value.length,
maxPrice: Math.max(0, ...filteredProducts.value.map(p => p.price)),
avgRating: filteredProducts.value.length
? filteredProducts.value.reduce((s, p) => s + p.rating, 0) / filteredProducts.value.length
: 0
}))
function resetFilters() {
filters.keyword = ''
filters.categories = []
filters.priceRange = [0, 10000]
filters.minRating = 0
filters.inStockOnly = false
}
function toggleCategory(cat: string) {
const i = filters.categories.indexOf(cat)
i >= 0 ? filters.categories.splice(i, 1) : filters.categories.push(cat)
}
let unwatch: (() => void) | null = null
onMounted(() => {
unwatch = watch(
() => [filters.keyword, filters.priceRange[1]],
async () => {
// 模拟统计上报
console.log('[analytics] filter changed')
},
{ deep: false, flush: 'post' }
)
})
onBeforeUnmount(() => {
unwatch?.()
})
return {
products,
loading,
error,
filters,
filteredProducts,
stats,
resetFilters,
toggleCategory
}
}
两种响应式声明对比
| 维度 | ref | reactive |
|---|---|---|
| 支持原始类型 | ✅ string/number/boolean 等 | ❌ 仅支持对象/数组 |
| 访问方式 | .value 读写 |
直接读写属性 |
| 解构赋值 | ✅ 保持响应性 | ❌ 丢失响应性(需 toRefs) |
| 整体替换 | obj.value = newObj 安全 |
Object.assign 手动合并 |
| TS 类型推断 | 直观 Ref<T> |
递归 unwrap 易混淆 |
| 内存开销 | 稍高(包装对象) | 略低(Proxy 直接包裹) |
Composable 抽取的三条黄金原则
代码示例中的 useProductFilter 就是一个典型的 Composable。好的 Composable 应该遵循:
- 单一职责:每个 Composable 只做一件事。不要写出
useProductFilterWithAuthAndAnalytics这样的怪物 - 显式返回值:使用
return { a, b, c }而非返回 reactive 对象,让调用方知道自己用了什么 - 生命周期对称:在 Composable 内部申请的资源(监听、定时器、事件订阅)一定要在 onBeforeUnmount 中释放
避免常见的性能陷阱
组合式 API 虽然灵活,但也容易踩坑:
// ❌ 错误:每次渲染都重新计算巨大数组
const sorted = [...products.value].sort(heavySortFn)
// ✅ 正确:用 computed 缓存结果
const sorted = computed(() => [...products.value].sort(heavySortFn))
// ❌ 错误:watch 开启 deep 监听大型对象
watch(hugeObject, handler, { deep: true })
// ✅ 更好:只监听关心的字段
watch(() => hugeObject.items[0].status, handler)
最佳实践
- 用
computed包装所有派生状态,不要在 template 里写复杂表达式 - 组合式函数命名统一
useXxx前缀,文件命名也对应useXxx.ts - Provide/Inject 搭配 Composable 使用,在根级 Provider 组件中统一管理全局状态
- 不要把 template 专用的计算逻辑放在 Composable 里,保持它的普适性
- 使用
unref()和isRef()编写既能接受 Ref 又能接受普通值的工具函数
总结
组合式 API 的核心价值在于"逻辑关注点聚合"与"复用粒度精细"。把 Options API 时代散落的 data/methods/computed/watch 按业务领域重新组织成一个个可测试、可组合的单元,才是它真正的威力所在。掌握 Composable 的设计,你的 Vue 代码质量会迎来质的飞跃。