告别重复代码!Vue 3 组合式函数(Composables )让你的开发更高效
·
告别重复代码!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)
}
// ...其他选项
}
更多推荐
所有评论(0)