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 的最佳实践
Composable 和 Pinia Store 都用于共享逻辑,区别在于:Composable 每次调用都创建独立的状态实例(适合组件级逻辑);Pinia Store 是单例,全局共享同一份状态(适合应用级状态)。在 Composable 中使用 ref 如果在模块作用域声明(不在函数内部),也会变成全局共享状态。
6.5 高级模式:带清理的 Composable
涉及副作用的 Composable(如事件监听、定时器、WebSocket)必须在组件卸载时清理资源,否则会造成内存泄漏。通过 onUnmounted 或 Vue 3.5 的 onWatcherCleanup 实现自动清理。
// composables/useEventListener.ts — 带自动清理的事件监听
export function useEventListener(
target: EventTarget | Ref<EventTarget>,
event: string,
handler: EventListenerOrEventListenerObject
) {
// 支持 ref 包裹的 DOM 元素
const resolvedTarget = isRef(target) ? target : ref(target)
watch(resolvedTarget, (el, _, onCleanup) => {
if (!el) return
el.addEventListener(event, handler)
// Vue 3.5+:onCleanup 在 watcher 重新运行前自动调用
onCleanup(() => el.removeEventListener(event, handler))
}, { immediate: true })
// 兜底:组件卸载时确保清理
onUnmounted(() => {
resolvedTarget.value?.removeEventListener(event, handler)
})
}
// composables/useWebSocket.ts — WebSocket 连接管理
export function useWebSocket(url: string) {
const data = ref<string | null>(null)
const status = ref<'connecting' | 'open' | 'closed'>('connecting')
let ws: WebSocket
onMounted(() => {
ws = new WebSocket(url)
ws.onopen = () => { status.value = 'open' }
ws.onclose = () => { status.value = 'closed' }
ws.onmessage = (e) => { data.value = e.data }
})
onUnmounted(() => ws?.close()) // 组件销毁时断开连接
function send(msg: string) {
if (status.value === 'open') ws.send(msg)
}
return { data, status, send }
}
6.6 测试 Composable
Composable 的可测试性是它相比 Options API Mixin 的重大优势。由于 Composable 是纯函数,可以直接在测试中调用,无需挂载组件。
import { describe, it, expect, vi } from 'vitest'
import { withSetup } from './test-utils'
import { useCounter } from '@/composables/useCounter'
// 辅助函数:创建模拟 setup 上下文
function withSetup<T>(composable: () => T): T {
let result!: T
defineComponent({
setup() { result = composable() }
})
return result
}
describe('useCounter', () => {
it('初始值为 0', () => {
const { count } = withSetup(() => useCounter())
expect(count.value).toBe(0)
})
it('increment 使 count +1', () => {
const { count, increment } = withSetup(() => useCounter())
increment()
expect(count.value).toBe(1)
})
it('支持自定义初始值', () => {
const { count } = withSetup(() => useCounter(10))
expect(count.value).toBe(10)
})
})
Composable 内部可能使用 watch、onMounted 等依赖调用顺序的 API。在条件语句或循环中调用 Composable 会破坏这种顺序一致性,导致生命周期钩子行为异常。规则:始终在 setup() 的同步代码顶层调用 Composable,不要在 if、for 或异步函数中调用。
Composables 是 Vue 3 Composition API 的最佳实践——将有状态的逻辑封装为可复用的函数。命名约定 useXxx,放在 composables/ 目录。相较于 Vue 2 的 Mixin,Composable 解决了命名冲突、来源不清晰和类型推导困难的问题。核心技巧:通过 onUnmounted 清理副作用(事件监听、定时器、WebSocket),用 toRefs() 或直接返回 refs 保持响应性,用 VueUse 库(80+ 现成 Composable)提升开发效率。