Hello
← 博客

Vue 3 Composables 实战:五个提升代码复用性的模式

在 Vue 3 的组合式 API 中,Composables 是实现逻辑复用的核心机制。相比 Vue 2 的 Mixins,它解决了命名冲突、来源不明确等痛点。本文总结了日常开发中最常用的五种模式。

模式一:响应式状态封装

最基础的模式是将一组相关的响应式状态和操作封装在一起:

import { ref, computed } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const doubled = computed(() => count.value * 2)

  function increment() { count.value++ }
  function reset() { count.value = initial }

  return { count, doubled, increment, reset }
}

关键点在于返回值使用 ref 而非 reactive,这样解构后仍保持响应性。

模式二:异步请求封装

几乎所有项目都需要处理 API 请求的加载、错误、数据三态:

import { ref, shallowRef } from 'vue'

export function useFetch<T>(url: string) {
  const data = shallowRef<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  execute()
  return { data, error, loading, refetch: execute }
}

使用 shallowRef 存储复杂数据可避免不必要的深层响应式转换。

模式三:跨组件状态共享

利用模块级变量实现轻量级的全局状态:

import { ref } from 'vue'

const globalCount = ref(0)

export function useSharedCounter() {
  function increment() { globalCount.value++ }
  return { count: globalCount, increment }
}

多个组件调用 useSharedCounter() 时共享同一个 globalCount。适合不需要 Pinia 的简单场景。

模式四:副作用自动清理

结合 onUnmounted 确保资源释放:

import { ref, onUnmounted } from 'vue'

export function useEventListener(
  target: EventTarget,
  event: string,
  handler: EventListener
) {
  target.addEventListener(event, handler)
  onUnmounted(() => target.removeEventListener(event, handler))
}

这个模式保证了组件卸载时自动解绑事件监听,避免内存泄漏。

模式五:组合 Composables

Composable 可以组合其他 Composable,构建更高层的抽象:

import { ref, watch } from 'vue'
import { useFetch } from './useFetch'
import { useDebounce } from './useDebounce'

export function useSearch(baseUrl: string) {
  const query = ref('')
  const debouncedQuery = useDebounce(query, 300)

  const { data, loading } = useFetch(
    computed(() => `${baseUrl}?q=${debouncedQuery.value}`)
  )

  return { query, results: data, loading }
}

总结

这五个模式覆盖了绝大多数场景。核心原则是:保持 Composable 职责单一、返回值使用 ref 保持解构安全、必要时清理副作用。掌握这些模式后,你会发现代码复用变得自然而优雅。