Chapter 06

组合式函数 Composables

Composition API 最强大的能力 — 将有状态的逻辑提取为可复用的函数,彻底告别 Mixins 的混乱

6.1 什么是 Composable?

Composable(组合式函数)是利用 Vue 的 Composition API 封装和复用有状态逻辑的函数。它是 Vue 3 对 Vue 2 Mixins 的根本性替代方案。

Mixins 的缺陷

  • 属性来源不明确(不知道某属性来自哪个 mixin)
  • 命名冲突(多个 mixin 有同名属性)
  • 难以传递参数
  • 隐式依赖,耦合度高

Composables 的优势

  • 明确的来源(const { x } = useMouse()
  • 无命名冲突(通过解构重命名解决)
  • 像普通函数一样传参
  • 纯函数,无隐式依赖

6.2 编写第一个 Composable

Composable 就是一个以 use 开头的函数,内部使用 Composition API,返回响应式状态和方法:

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

// useXxx 命名约定
export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e: MouseEvent) {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 生命周期钩子在 composable 内部也可以使用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }  // 返回响应式 refs
}
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()  // 来源清晰
</script>
<template>
  <p>鼠标位置:{{ x }}, {{ y }}</p>
</template>

6.3 实用 Composable 模式

useAsyncState — 异步数据加载

import { ref, shallowRef } from 'vue'

export function useAsyncState<T>(
  asyncFn: () => Promise<T>,
  defaultValue: T
) {
  const state     = shallowRef<T>(defaultValue)
  const isLoading = ref(false)
  const error     = ref<Error | null>(null)

  async function execute() {
    isLoading.value = true
    error.value = null
    try {
      state.value = await asyncFn()
    } catch (e) {
      error.value = e as Error
    } finally {
      isLoading.value = false
    }
  }

  execute()  // 立即执行
  return { state, isLoading, error, execute }
}

// 使用
const { state: users, isLoading, error, execute: refresh } = useAsyncState(
  () => fetch('/api/users').then(r => r.json()),
  []
)

useLocalStorage — 持久化状态

import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  // 自动同步到 localStorage
  watch(data, (val) => {
    if (val === null) {
      localStorage.removeItem(key)
    } else {
      localStorage.setItem(key, JSON.stringify(val))
    }
  }, { deep: true })

  return data
}

// 使用:自动持久化的主题偏好
const theme = useLocalStorage<'light' | 'dark'>('theme', 'light')

useDebounce — 防抖

import { ref, watch } from 'vue'
import type { Ref } from 'vue'

export function useDebounce<T>(value: Ref<T>, delay = 300) {
  const debouncedValue = ref<T>(value.value) as Ref<T>

  let timer: ReturnType<typeof setTimeout>

  watch(value, (val) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      debouncedValue.value = val
    }, delay)
  })

  return debouncedValue
}

// 使用
const searchInput = ref('')
const debouncedSearch = useDebounce(searchInput, 500)
watch(debouncedSearch, performSearch)  // 500ms 后才触发搜索

6.4 VueUse — 社区最强工具库

VueUse 是一个提供超过 200 个常用 Composables 的社区库,涵盖浏览器 API、传感器、动画、状态管理等各类场景。

npm install @vueuse/core
<script setup>
import {
  useDark,          // 暗色模式
  useToggle,        // 布尔切换
  useFetch,         // 数据请求
  useLocalStorage,  // 持久化存储
  useIntersectionObserver, // 元素可见性
  useGeolocation,   // 地理位置
  useClipboard,     // 剪贴板
  useEventListener, // 事件监听(自动清理)
  useWindowSize,    // 窗口尺寸
  useIntervalFn,    // 定时器
  useVModel,        // v-model 助手
} from '@vueuse/core'

// 暗色模式(自动同步 class 到 <html>)
const isDark   = useDark()
const toggleDark = useToggle(isDark)

// 自动请求(响应式 URL)
const userId = ref(1)
const { data, isFetching, error } = useFetch(
  () => `/api/users/${userId.value}`  // userId 变化自动重新请求
).json()

// 懒加载图片(进入视口才加载)
const imgRef = ref()
const { stop } = useIntersectionObserver(imgRef, ([entry]) => {
  if (entry.isIntersecting) {
    imgRef.value.src = imgRef.value.dataset.src
    stop()  // 加载后停止观察
  }
})

// 自动清理的事件监听
useEventListener('keydown', (e: KeyboardEvent) => {
  if (e.key === 'Escape') closeModal()
})  // 组件卸载时自动移除
</script>

6.5 Composable 的最佳实践

use 前缀
命名约定:Composable 函数以 use 开头(useFetch、useAuth、useCart),这有助于区分普通工具函数和包含响应式逻辑的 Composable。
返回响应式引用
返回 ref 而不是原始值,这样调用方可以解构并保持响应性。即使是对象也建议 toRefs 处理后返回。
接受 ref 参数
参数可以接受 ref 或普通值(使用 toValue() / unref() 处理),使 Composable 更灵活:const id = toValue(props)。
清理副作用
在 onUnmounted 中清理事件监听、定时器、WebSocket 连接等,防止内存泄漏。VueUse 的工具会自动处理这些。
避免在非 setup 中调用
Composable 必须在 setup() 或 <script setup> 的同步执行上下文中调用,不能在异步函数或事件处理器中调用生命周期钩子。
Composable vs Store

Composable 和 Pinia Store 都用于共享逻辑,区别在于:Composable 每次调用都创建独立的状态实例(适合组件级逻辑);Pinia Store 是单例,全局共享同一份状态(适合应用级状态)。在 Composable 中使用 ref 如果在模块作用域声明(不在函数内部),也会变成全局共享状态。