Vue 3 组合式 API 实战

Choyeon· 2026年8月21日· 2 分钟阅读· 619 阅读· 601 字· 869 字符
Vue 3 组合式 API 实战

自 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 应该遵循:

  1. 单一职责:每个 Composable 只做一件事。不要写出 useProductFilterWithAuthAndAnalytics 这样的怪物
  2. 显式返回值:使用 return { a, b, c } 而非返回 reactive 对象,让调用方知道自己用了什么
  3. 生命周期对称:在 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)

最佳实践

  1. 用 computed 包装所有派生状态,不要在 template 里写复杂表达式
  2. 组合式函数命名统一 useXxx 前缀,文件命名也对应 useXxx.ts
  3. Provide/Inject 搭配 Composable 使用,在根级 Provider 组件中统一管理全局状态
  4. 不要把 template 专用的计算逻辑放在 Composable 里,保持它的普适性
  5. 使用 unref() 和 isRef() 编写既能接受 Ref 又能接受普通值的工具函数

总结

组合式 API 的核心价值在于"逻辑关注点聚合"与"复用粒度精细"。把 Options API 时代散落的 data/methods/computed/watch 按业务领域重新组织成一个个可测试、可组合的单元,才是它真正的威力所在。掌握 Composable 的设计,你的 Vue 代码质量会迎来质的飞跃。

本文作者

评论 (0)

暂无评论,来抢沙发吧。