告别重复代码!Vue 3 组合式函数(Composables )让你的开发更高效

1. 什么是组合式函数?

1.1 概念定义

组合式函数是利用 Vue 3 的组合式 API 来封装和复用有状态逻辑的函数。

1.2 与传统工具函数的区别

// 工具函数 - 无状态逻辑
function formatDate(date) {
  return dayjs(date).format('YYYY-MM-DD')
}

// 组合式函数 - 包含响应式状态
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

1.3 与 Mixins 的对比

特性 Mixins Composables
命名冲突 容易发生 可自定义命名
隐式依赖 不明确 显式导入
类型支持 有限 完整的 TypeScript 支持
代码组织 选项合并 逻辑组合

2. 基础组合式函数

2.1 计数器示例

import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const double = computed(() => count.value * 2)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  const set = (value: number) => count.value = value
  
  return {
    count,
    double,
    increment,
    decrement,
    reset,
    set
  }
}

2.2 在组件中使用

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ double }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

<script setup lang="ts">
import { useCounter } from './composables/useCounter'

const { count, double, increment, decrement, reset } = useCounter(10)
</script>

3. 响应式状态管理

3.1 使用 ref 和 reactive

import { ref, reactive, toRefs } from 'vue'

export function useUser() {
  // 使用 ref 处理基本类型
  const userId = ref(null)
  
  // 使用 reactive 处理对象
  const userState = reactive({
    name: '',
    email: '',
    avatar: '',
    isActive: false
  })
  
  // 转换为 refs 以便解构
  return {
    userId,
    ...toRefs(userState)
  }
}

3.2 状态更新模式

export function useForm(initialState = {}) {
  const form = reactive({ ...initialState })
  
  // 更新单个字段
  const updateField = (field: string, value: any) => {
    form[field] = value
  }
  
  // 批量更新
  const updateFields = (updates: Record<string, any>) => {
    Object.assign(form, updates)
  }
  
  // 重置表单
  const reset = () => {
    Object.assign(form, initialState)
  }
  
  return {
    form,
    updateField,
    updateFields,
    reset
  }
}

4. 生命周期集成

4.1 自动生命周期管理

import { onMounted, onUnmounted, ref } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  const update = (event: MouseEvent) => {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  
  return { x, y }
}

4.2 手动生命周期控制

export function useEventListener(target, event, callback) {
  const register = () => {
    target.addEventListener(event, callback)
  }
  
  const unregister = () => {
    target.removeEventListener(event, callback)
  }
  
  return {
    register,
    unregister
  }
}

5. 异步操作处理

5.1 数据获取模式

import { ref, watch } from 'vue'

export function useApi(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async (params = {}) => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, {
        method: options.method || 'GET',
        body: options.method === 'POST' ? JSON.stringify(params) : null,
        headers: options.headers || {}
      })
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return {
    data,
    loading,
    error,
    execute
  }
}

5.2 更高级的异步组合式函数

import { ref, computed } from 'vue'

export function useAsync(fn, options = {}) {
  const {
    immediate = true,
    initialData = null,
    onSuccess,
    onError
  } = options
  
  const data = ref(initialData)
  const loading = ref(false)
  const error = ref(null)
  
  const isSuccess = computed(() => !error.value && !loading.value)
  const isError = computed(() => !!error.value)
  
  const execute = async (...args) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await fn(...args)
      data.value = result
      onSuccess?.(result)
      return result
    } catch (err) {
      error.value = err
      onError?.(err)
      throw err
    } finally {
      loading.value = false
    }
  }
  
  // 立即执行
  if (immediate) {
    execute()
  }
  
  return {
    data,
    loading,
    error,
    execute,
    isSuccess,
    isError
  }
}

6. 副作用管理

6.1 Watch 和 WatchEffect

import { watch, watchEffect, ref } from 'vue'

export function useSearch(searchTerm, apiCall) {
  const results = ref([])
  const loading = ref(false)
  
  // 使用 watch 精确控制
  watch(searchTerm, async (newTerm, oldTerm, onCleanup) => {
    if (!newTerm.trim()) {
      results.value = []
      return
    }
    
    loading.value = true
    const controller = new AbortController()
    
    onCleanup(() => {
      controller.abort()
    })
    
    try {
      results.value = await apiCall(newTerm, controller.signal)
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Search failed:', error)
      }
    } finally {
      loading.value = false
    }
  }, { immediate: true })
  
  return { results, loading }
}

6.2 自动清理副作用

export function useInterval(callback, delay) {
  const intervalId = ref(null)
  
  const start = () => {
    intervalId.value = setInterval(callback, delay)
  }
  
  const stop = () => {
    if (intervalId.value) {
      clearInterval(intervalId.value)
      intervalId.value = null
    }
  }
  
  // 自动清理
  onUnmounted(stop)
  
  return {
    start,
    stop,
    isRunning: computed(() => intervalId.value !== null)
  }
}

7. 组合式函数的组合

7.1 组合多个组合式函数

export function useUserDashboard(userId) {
  // 组合其他 composables
  const { data: user, loading: userLoading } = useUser(userId)
  const { data: posts, loading: postsLoading } = useUserPosts(userId)
  const { data: stats } = useUserStats(userId)
  
  // 计算属性
  const isLoading = computed(() => userLoading.value || postsLoading.value)
  
  // 方法
  const refreshAll = async () => {
    await Promise.all([
      refreshUser(),
      refreshPosts(),
      refreshStats()
    ])
  }
  
  return {
    user,
    posts,
    stats,
    isLoading,
    refreshAll
  }
}

7.2 可配置的组合式函数

export function usePagination(api, options = {}) {
  const {
    initialPage = 1,
    pageSize = 10,
    immediate = true
  } = options
  
  const currentPage = ref(initialPage)
  const pageSizeRef = ref(pageSize)
  
  const { data, loading, error, execute } = useAsync(
    () => api({ page: currentPage.value, size: pageSizeRef.value }),
    { immediate }
  )
  
  const total = computed(() => data.value?.total || 0)
  const items = computed(() => data.value?.items || [])
  
  const totalPages = computed(() => 
    Math.ceil(total.value / pageSizeRef.value)
  )
  
  const canGoNext = computed(() => 
    currentPage.value < totalPages.value
  )
  
  const canGoPrev = computed(() => 
    currentPage.value > 1
  )
  
  const goToPage = (page: number) => {
    currentPage.value = Math.max(1, Math.min(page, totalPages.value))
    execute()
  }
  
  const nextPage = () => {
    if (canGoNext.value) {
      goToPage(currentPage.value + 1)
    }
  }
  
  const prevPage = () => {
    if (canGoPrev.value) {
      goToPage(currentPage.value - 1)
    }
  }
  
  return {
    // 状态
    currentPage,
    pageSize: pageSizeRef,
    items,
    total,
    loading,
    error,
    
    // 计算属性
    totalPages,
    canGoNext,
    canGoPrev,
    
    // 方法
    goToPage,
    nextPage,
    prevPage,
    refresh: execute
  }
}

8. TypeScript 集成

8.1 完整的类型定义

import { Ref, ComputedRef } from 'vue'

interface UseCounterReturn {
  count: Ref<number>
  double: ComputedRef<number>
  increment: () => void
  decrement: () => void
  reset: () => void
  set: (value: number) => void
}

interface UseAsyncOptions<T> {
  immediate?: boolean
  initialData?: T
  onSuccess?: (data: T) => void
  onError?: (error: any) => void
}

interface UseAsyncReturn<T> {
  data: Ref<T | null>
  loading: Ref<boolean>
  error: Ref<string | null>
  execute: (...args: any[]) => Promise<T | void>
  isSuccess: ComputedRef<boolean>
  isError: ComputedRef<boolean>
}

export function useCounter(initialValue?: number): UseCounterReturn

export function useAsync<T>(
  fn: (...args: any[]) => Promise<T>,
  options?: UseAsyncOptions<T>
): UseAsyncReturn<T>

8.2 泛型组合式函数

export function useFetch<T>(url: string): UseAsyncReturn<T> {
  return useAsync<T>(async () => {
    const response = await fetch(url)
    if (!response.ok) {
      throw new Error('Network response was not ok')
    }
    return response.json()
  })
}

9. 测试策略

9.1 单元测试示例

import { describe, it, expect, vi } from 'vitest'
import { renderHook } from '@vue/test-utils'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should initialize with default value', () => {
    const { result } = renderHook(() => useCounter())
    expect(result.count.value).toBe(0)
  })
  
  it('should increment counter', async () => {
    const { result } = renderHook(() => useCounter(5))
    
    await result.increment()
    
    expect(result.count.value).toBe(6)
  })
  
  it('should compute double value', () => {
    const { result } = renderHook(() => useCounter(3))
    
    expect(result.double.value).toBe(6)
  })
})

9.2 测试异步组合式函数

import { useAsync } from './useAsync'

describe('useAsync', () => {
  it('should handle successful request', async () => {
    const mockApi = vi.fn().mockResolvedValue('test data')
    
    const { result } = renderHook(() => useAsync(mockApi))
    
    await result.execute()
    
    expect(result.data.value).toBe('test data')
    expect(result.loading.value).toBe(false)
    expect(result.error.value).toBe(null)
  })
})

10. 最佳实践和模式

10.1 命名约定

// ✅ 好的命名
useCounter
useMousePosition
useApiRequest
useLocalStorage

// ❌ 不好的命名
counter // 缺少 use 前缀
getMouse // 动词开头
api // 含义不明确

10.2 参数设计

// 使用选项对象参数
export function useFeature(options = {}) {
  const {
    enabled = true,
    duration = 1000,
    onSuccess,
    onError
  } = options
  
  // ... 实现
}

// 使用方式
useFeature({
  enabled: true,
  duration: 2000,
  onSuccess: (data) => console.log('Success:', data)
})

10.3 返回值设计

export function useComplexFeature() {
  const state = reactive({
    data: null,
    loading: false,
    error: null
  })
  
  const computedValue = computed(() => state.data?.value)
  
  const methods = {
    async fetch() { /* ... */ },
    reset() { /* ... */ }
  }
  
  return {
    ...toRefs(state),
    computedValue,
    ...methods
  }
}

10.4 错误处理模式

export function useSafeComposable(composable, fallback = null) {
  try {
    return composable()
  } catch (error) {
    console.error('Composable error:', error)
    return fallback
  }
}

11. 实际应用示例

11.1 本地存储组合式函数

import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue = null) {
  const data = ref(defaultValue)
  
  // 从 localStorage 读取初始值
  try {
    const item = window.localStorage.getItem(key)
    data.value = item ? JSON.parse(item) : defaultValue
  } catch (error) {
    console.error(`Error reading localStorage key "${key}":`, error)
    data.value = defaultValue
  }
  
  // 监听变化并保存到 localStorage
  watch(data, (newValue) => {
    try {
      if (newValue === null || newValue === undefined) {
        window.localStorage.removeItem(key)
      } else {
        window.localStorage.setItem(key, JSON.stringify(newValue))
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error)
    }
  }, { deep: true })
  
  const remove = () => {
    data.value = null
  }
  
  return {
    data,
    remove
  }
}

11.2 网络状态检测

import { ref, onMounted, onUnmounted } from 'vue'

export function useNetwork() {
  const isOnline = ref(navigator.onLine)
  
  const updateOnlineStatus = () => {
    isOnline.value = navigator.onLine
  }
  
  onMounted(() => {
    window.addEventListener('online', updateOnlineStatus)
    window.addEventListener('offline', updateOnlineStatus)
  })
  
  onUnmounted(() => {
    window.removeEventListener('online', updateOnlineStatus)
    window.removeEventListener('offline', updateOnlineStatus)
  })
  
  return {
    isOnline,
    isOffline: computed(() => !isOnline.value)
  }
}

12. 性能优化

12.1 懒加载组合式函数

export function useLazyComposable(composableFactory) {
  const composable = ref(null)
  
  const load = async () => {
    if (!composable.value) {
      const module = await composableFactory()
      composable.value = module.default || module
    }
    return composable.value
  }
  
  return {
    load,
    isLoaded: computed(() => !!composable.value)
  }
}

12.2 记忆化计算

import { computed, ref } from 'vue'

export function useMemoizedComputation(computeFn, dependencies) {
  const cache = ref(new Map())
  
  return computed(() => {
    const key = JSON.stringify(dependencies.map(dep => dep.value))
    
    if (cache.value.has(key)) {
      return cache.value.get(key)
    }
    
    const result = computeFn()
    cache.value.set(key, result)
    return result
  })
}

在选项式 API 中使用组合式函数

如果你正在使用选项式 API,组合式函数必须在 setup() 中调用。且其返回的绑定必须在 setup() 中返回,以便暴露给 this 及其模板:

import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup() 暴露的属性可以在通过 `this` 访问到
    console.log(this.x)
  }
  // ...其他选项
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐