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 如果在模块作用域声明(不在函数内部),也会变成全局共享状态。