Vue3组合式函数Composable的核心设计原则
Vue3的Composition API让逻辑复用从Mixin混入模式进化为Composable函数模式。一个合格的Composable必须遵循三个设计原则:显式依赖注入(通过参数传入ref和props)、单一职责(一个Composable只做一件事)、可测试性(不依赖组件实例上下文)。违反这三个原则的Composable最终会变成另一种形式的Mixin——隐式依赖、职责混杂、测试困难。
Composable的标准命名约定是use前缀,如useCounter、useFetch、useIntersectionObserver。这个约定不只是风格偏好,而是Vue生态的工具链(VueDevTools、TypeScript类型推导)都依赖这个前缀来识别组合式函数。
基础Composable:状态与逻辑封装
以一个带防抖的搜索输入为例:
// composables/useDebouncedSearch.ts
import { ref, watch, type Ref } from 'vue'
export function useDebouncedSearch(delay: number = 300) {
const searchQuery = ref('')
const debouncedQuery = ref('')
let timer: ReturnType<typeof setTimeout> | null = null
watch(searchQuery, (newVal) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
debouncedQuery.value = newVal
}, delay)
})
return { searchQuery, debouncedQuery }
}
使用方式:
const { searchQuery, debouncedQuery } = useDebouncedSearch(500)
// 模板中绑定searchQuery,watch debouncedQuery发请求
watch(debouncedQuery, async (query) => {
if (query) {
const results = await searchApi(query)
// ...
}
})
这个Composable的好处是搜索防抖逻辑完全内聚,组件只需消费结果,不需要关心setTimeout的清理逻辑。
进阶模式:异步数据获取Composable
数据获取是前端最常见的需求。封装一个通用的useFetch需要处理加载状态、错误处理、数据缓存和请求取消:
// composables/useFetch.ts
import { ref, shallowRef, watchEffect, type Ref } from 'vue'
interface UseFetchOptions<T> {
immediate?: boolean
initialData?: T
}
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
loading: Ref<boolean>
execute: () => Promise<void>
}
export function useFetch<T>(
url: Ref<string> | string,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const { immediate = true, initialData = null } = options
const data = shallowRef<T | null>(initialData)
const error = ref<Error | null>(null)
const loading = ref(false)
let controller: AbortController | null = null
const execute = async () => {
const resolvedUrl = typeof url === 'string' ? url : url.value
if (!resolvedUrl) return
if (controller) controller.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
const response = await fetch(resolvedUrl, { signal: controller.signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json() as T
} catch (e: any) {
if (e.name !== 'AbortError') {
error.value = e
}
} finally {
loading.value = false
controller = null
}
}
if (immediate) {
if (typeof url === 'string') {
execute()
} else {
watchEffect(() => execute())
}
}
return { data, error, loading, execute }
}
关键设计点:使用shallowRef避免深层响应式对大JSON对象的性能损耗;AbortController确保快速切换时取消旧请求;watchEffect自动追踪URL变化重新请求。
高阶模式:Composable的组合与条件渲染
Composable真正的威力在于组合。以一个列表页为例,需要分页、搜索、无限滚动三种能力:
// composables/usePagination.ts
export function usePagination(pageSize: number = 20) {
const currentPage = ref(1)
const total = ref(0)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value++
}
const prevPage = () => {
if (currentPage.value > 1) currentPage.value--
}
return { currentPage, total, totalPages, nextPage, prevPage, pageSize }
}
// composables/useInfiniteScroll.ts
export function useInfiniteScroll(
loadMore: () => Promise<void>, threshold = 100
) {
const isIntersecting = ref(false)
const observer = ref<IntersectionObserver | null>(null)
const observe = (el: HTMLElement) => {
observer.value = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isIntersecting.value) {
isIntersecting.value = true
loadMore().finally(() => { isIntersecting.value = false })
}
},
{ rootMargin: `${threshold}px` }
)
observer.value.observe(el)
}
onUnmounted(() => observer.value?.disconnect())
return { isIntersecting, observe }
}
// 组件中组合使用
const { currentPage, total, nextPage } = usePagination(20)
const { debouncedQuery } = useDebouncedSearch(500)
const { data, loading, execute } = useFetch(computed(
() => `/api/items?page=${currentPage.value}&q=${debouncedQuery.value}`
))
const { observe } = useInfiniteScroll(async () => {
nextPage()
await execute()
})
三个Composable各管各的职责,通过ref自动联动:搜索输入触发debouncedQuery变化,进而触发useFetch的URL计算属性变化,自动重新请求;滚动到底部触发nextPage改变currentPage,URL变化后自动重新请求。整个数据流无手动事件派发。
Composable的TypeScript类型约束
生产级Composable必须有完整的类型定义。核心原则是输入参数尽量用具体类型,返回值尽量用Ref的泛型:
interface UseListOptions<T> {
fetchFn: (page: number, query: string) =>
Promise<{ data: T[]; total: number }>
pageSize?: number
immediate?: boolean
}
export function useList<T>(options: UseListOptions<T>) {
const items = shallowRef<T[]>([]) as Ref<T[]>
// ...
return { items } as const
}
使用as const返回类型确保TypeScript将返回值推断为readonly元组,而不是宽松的联合类型。这样解构时每个ref的类型都是精确的,不会丢失泛型信息。
避免Composable反模式
1. 不要在Composable内部直接操作DOM。如果需要DOM引用,让调用者通过ref传入。
2. 不要在Composable中使用全局状态(如Pinia store),通过参数注入依赖。
3. 不要把Composable写成类。Vue的响应式系统基于Proxy,class的getter/setter可能无法正确触发依赖收集,用普通函数加ref替代。
4. 异步Composable必须在setup顶层同步调用。如果需要条件调用,把条件判断放在Composable内部,不要用if包裹useXxx()调用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3composable-she-ji-mo-shi-cong-ji-chu-feng-zhuang-dao/