组合式API不是Option API的语法糖
Vue3的组合式API(Composition API)经常被当作Option API的替代写法来使用——把data放到ref、methods放到普通函数、生命周期放到onMounted,逻辑结构没有任何改变。这种用法完全浪费了组合式API的核心价值:逻辑聚合与复用。
组合式API的真正优势是按功能维度组织代码,而不是按选项类型。一个用户认证功能的状态、方法、副作用全部聚合在一起,而不是分散在data、methods、computed各个选项中。这种组织方式使得逻辑提取为可复用的Hook成为可能。
自定义Hook的设计原则
好的Hook设计需要遵循三个原则:单一职责、显式依赖、可控副作用。
单一职责要求一个Hook只做一件事。useAuth负责认证,useFetch负责数据请求,usePagination负责分页逻辑。把认证和数据请求混在一个useAuthAndFetch中,看似方便,实际上降低了复用性。
显式依赖意味着Hook的参数就是它的依赖项,不隐式依赖全局状态或路由参数:
// 不好的设计:隐式依赖路由参数
function useArticleDetail() {
const route = useRoute()
const id = route.params.id // 隐式依赖
const article = ref(null)
// ...
}
// 好的设计:显式传入依赖
function useArticleDetail(id: Ref<string>) {
const article = ref(null)
watch(id, (newId) => {
fetchArticle(newId).then(data => article.value = data)
}, { immediate: true })
return { article }
}
可控副作用指Hook内部创建的定时器、事件监听、WebSocket连接等必须在Hook销毁时自动清理:
function useWebSocket(url: Ref<string>) {
const data = ref(null)
const status = ref('connecting')
let ws: WebSocket | null = null
const connect = () => {
ws = new WebSocket(url.value)
ws.onmessage = (e) => { data.value = JSON.parse(e.data) }
ws.onopen = () => { status.value = 'connected' }
ws.onclose = () => { status.value = 'disconnected' }
}
watch(url, () => {
ws?.close()
connect()
}, { immediate: true })
// 副作用自动清理
onUnmounted(() => {
ws?.close()
})
const send = (msg: unknown) => {
ws?.send(JSON.stringify(msg))
}
return { data, status, send }
}
请求Hook的通用设计:useRequest
数据请求是前端最常见的需求,一个设计良好的useRequest Hook可以覆盖90%的请求场景:
interface UseRequestOptions<T> {
immediate?: boolean // 是否立即执行
debounceMs?: number // 防抖延迟
retryCount?: number // 失败重试次数
cache?: boolean // 是否缓存结果
cacheTime?: number // 缓存时间(ms)
onError?: (err: Error) => void
onSuccess?: (data: T) => void
}
function useRequest<T>(
fn: () => Promise<T>,
options: UseRequestOptions<T> = {}
) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(false)
const cacheMap = new Map<string, { data: T; expire: number }>()
const execute = async () => {
const cacheKey = fn.toString()
// 缓存检查
if (options.cache) {
const cached = cacheMap.get(cacheKey)
if (cached && cached.expire > Date.now()) {
data.value = cached.data
return
}
}
loading.value = true
error.value = null
let attempts = 0
const maxAttempts = (options.retryCount || 0) + 1
while (attempts < maxAttempts) {
try {
const result = await fn()
data.value = result
if (options.cache) {
cacheMap.set(cacheKey, {
data: result,
expire: Date.now() + (options.cacheTime || 60000)
})
}
options.onSuccess?.(result)
break
} catch (e) {
attempts++
if (attempts >= maxAttempts) {
error.value = e as Error
options.onError?.(e as Error)
}
}
}
loading.value = false
}
if (options.immediate !== false) {
execute()
}
return { data, error, loading, execute }
}
使用时只需传入异步函数和配置项:
const { data: userList, loading, error } = useRequest(
() => api.getUsers({ page: 1, size: 20 }),
{ immediate: true, cache: true, retryCount: 2 }
)
响应式性能陷阱:避免不必要的重新渲染
组合式API中常见的性能问题是大对象的深度响应式和computed的过度使用。
问题一:大型列表的深度响应式。1000条数据的列表用reactive包裹后,任何一条数据的任何字段变化都会触发依赖它的组件重新渲染。对于只读展示的列表,shallowRef更合适:
// 不好的做法:1000条数据全部深度响应式
const list = reactive(await fetchList()) // 性能差
// 好的做法:只在需要修改时使用深度响应式
const list = shallowRef(await fetchList())
// 修改某一条时手动触发更新
function updateItem(index, newData) {
const newList = [...list.value]
newList[index] = { ...newList[index], ...newData }
list.value = newList // 浅层替换触发更新
}
问题二:computed函数内的复杂计算。computed会缓存结果,但getter函数中的复杂计算每次依赖变化时都会重新执行。如果计算逻辑重,使用手动缓存:
// 计算密集型场景用watch + ref替代computed
const sortedList = ref([])
watch(
sourceList,
(list) => {
requestIdleCallback(() => {
sortedList.value = heavySort(list)
})
},
{ immediate: true }
)
requestIdleCallback将排序计算放到浏览器空闲时段执行,避免阻塞用户交互。
Hook组合:构建业务逻辑层
多个基础Hook可以组合出面向业务的复合Hook:
// 复合Hook:分页列表
function usePaginatedList<T>(fetchFn: (params: any) => Promise<T>) {
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const { data: rawdata, loading, error, execute } = useRequest(
() => fetchFn({ page: page.value, pageSize: pageSize.value }),
{ immediate: true }
)
const list = computed(() => rawdata.value?.items || [])
watch([page, pageSize], () => execute())
const gotoPage = (p: number) => { page.value = p }
const changeSize = (s: number) => { pageSize.value = s; page.value = 1 }
return { list, loading, error, page, pageSize, total, gotoPage, changeSize }
}
基础Hook保持通用性,复合Hook封装业务逻辑。这种分层设计让基础Hook可以在不同业务场景中复用,而复合Hook让页面组件的代码量大幅减少。
Hook测试策略
Hook的单元测试使用@vue/test-utils的mount组合setup函数:
import { mount } from '@vue/test-utils'
function withSetup<T>(hook: () => T): T {
let result: T
const wrapper = mount({
setup() {
result = hook()
return () => null
}
})
return result!
}
// 测试useRequest
test('useRequest应该正确处理请求', async () => {
const { data, loading, execute } = withSetup(() =>
useRequest(() => Promise.resolve({ name: 'test' }))
)
expect(loading.value).toBe(true)
await flushPromises()
expect(data.value).toEqual({ name: 'test' })
expect(loading.value).toBe(false)
})
withSetup辅助函数让Hook测试脱离组件上下文,专注验证Hook本身的逻辑正确性。每个Hook的测试只关注输入输出和副作用,不关心UI渲染细节。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-jin-jie-shi-zhan-zi-ding-yi-hook-she-ji/