组合式API下Hooks设计的核心问题
Vue3的组合式API(Composition API)让逻辑复用从Mixin升级到Hooks,但Hooks设计远不止”把setup里的代码抽出去”。差的Hooks导致响应式丢失、内存泄漏、重复请求。好的Hooks做到三点:响应式安全、生命周期解耦、接口极简。
常见反模式:
// 反模式1:解构丢失响应式
function useUser() {
const state = reactive({ name: '', age: 0 })
return { ...state } // 解构后name和age不再是响应式!
}
// 反模式2:未清理副作用
function useWebSocket(url) {
const ws = new WebSocket(url)
ws.onmessage = (e) => { /* ... */ }
// 组件卸载时ws不会断开 → 内存泄漏
}
// 反模式3:每次调用创建新实例
function useCounter() {
return { count: ref(0) } // 每个组件调用都创建独立的count
}
Hooks设计五原则
原则一:返回值保持引用稳定
computed和ref返回引用稳定的对象,解构不会丢失响应式:
// 正确:返回ref/computed
function useUser(userId: Ref<string>) {
const user = computed(() => fetchUser(userId.value))
const loading = ref(false)
return { user, loading } // ref和computed解构后仍然响应式
}
// 如果必须返回reactive对象,用toRefs
function useForm() {
const state = reactive({
username: '',
password: '',
remember: false
})
return toRefs(state) // 每个属性都变成ref
}
原则二:副作用必须可清理
function useWebSocket(url: string) {
const messages = ref<any[]>([])
const status = ref<'connecting' | 'open' | 'closed'>('connecting')
let ws: WebSocket | null = null
let reconnectTimer: number | null = null
const connect = () => {
ws = new WebSocket(url)
ws.onopen = () => { status.value = 'open' }
ws.onmessage = (e) => { messages.value.push(JSON.parse(e.data)) }
ws.onclose = () => {
status.value = 'closed'
// 自动重连
reconnectTimer = window.setTimeout(connect, 3000)
}
}
// 关键:onScopeDispose清理
onScopeDispose(() => {
reconnectTimer && clearTimeout(reconnectTimer)
ws?.close()
ws = null
})
connect()
return { messages, status, reconnect: connect }
}
原则三:参数支持响应式
Hooks参数如果是Ref,内部用watchEffect自动追踪变化:
function useFetch<T>(url: Ref<string> | ComputedRef<string>) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(false)
// 参数是响应式时自动重请求
watchEffect(async (onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
loading.value = true
error.value = null
try {
const res = await fetch(url.value, { signal: controller.signal })
data.value = await res.json()
} catch (e) {
if (e instanceof Error && e.name !== 'AbortError') {
error.value = e
}
} finally {
loading.value = false
}
})
return { data, error, loading }
}
原则四:避免重复调用(全局状态模式)
某些Hooks全局只需要一份实例(如WebSocket连接、全局主题):
// 全局单例Hooks
const globalState = () => {
let instance: { count: Ref<number> } | null = null
return () => {
if (!instance) {
instance = { count: ref(0) }
}
return instance
}
}
const useGlobalCounter = globalState()
// 在任意组件中调用,共享同一份状态
const { count } = useGlobalCounter()
原则五:返回可执行方法而非仅数据
function usePagination(fetchFn: (page: number) => Promise<any>) {
const page = ref(1)
const pageSize = ref(20)
const total = ref(0)
const list = ref<any[]>([])
const loading = ref(false)
const refresh = async () => {
loading.value = true
try {
const res = await fetchFn(page.value)
list.value = res.data
total.value = res.total
} finally {
loading.value = false
}
}
const nextPage = () => { page.value++; refresh() }
const prevPage = () => { page.value = Math.max(1, page.value - 1); refresh() }
const setPageSize = (size: number) => { pageSize.value = size; page.value = 1; refresh() }
return { page, pageSize, total, list, loading, refresh, nextPage, prevPage, setPageSize }
}
高阶Hooks组合模式
将基础Hooks组合成复杂业务Hooks:
// 基础Hooks:useDebounce
function useDebounce<T>(value: Ref<T>, delay = 300) {
const debouncedValue = ref(value.value) as Ref<T>
let timer: number | null = null
watch(value, (newVal) => {
if (timer) clearTimeout(timer)
timer = window.setTimeout(() => {
debouncedValue.value = newVal
}, delay)
})
onScopeDispose(() => { timer && clearTimeout(timer) })
return debouncedValue
}
// 基础Hooks:useFetch
// ... (同上)
// 组合Hooks:搜索自动补全
function useSearchSuggestions(baseUrl: string) {
const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 400)
const searchUrl = computed(() =>
debouncedKeyword.value ? `${baseUrl}?q=${debouncedKeyword.value}` : ''
)
const { data, loading, error } = useFetch<string[]>(searchUrl)
const suggestions = computed(() => data.value || [])
return { keyword, suggestions, loading, error }
}
性能优化关键点
shallowRef替代ref——大对象只追踪顶层引用变化:
// 大型列表用shallowRef,避免深层代理的性能开销
const hugeList = shallowRef<Item[]>([])
// 更新时直接替换整个数组
hugeList.value = [...newList]
computed缓存——computed只在依赖变化时重新计算,避免模板中的复杂表达式:
// 差:模板中每次渲染都计算
// <div>{{ items.filter(i => i.active).map(i => i.name).join(', ') }}</div>
// 好:computed自动缓存
const activeNames = computed(() =>
items.value.filter(i => i.active).map(i => i.name).join(', ')
)
虚拟滚动——长列表必须用虚拟滚动:
// 配合vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller'
// <RecycleScroller
// :items="largeList"
// :item-size="50"
// key-field="id"
// v-slot="{ item }"
// >
// <ListItem :data="item" />
// </RecycleScroller>
defineAsyncComponent——路由级组件按需加载:
const HeavyChart = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
v-memo指令——Vue3.2+的列表项级缓存:
// <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
// <!-- 只有item.selected变化时才重新渲染 -->
// </div>
TypeScript类型增强
Hooks的类型定义决定了开发体验:
// 通用Hooks类型定义
interface UseFetchReturn<T> {
data: Readonly<Ref<T | null>>
error: Readonly<Ref<Error | null>>
loading: Readonly<Ref<boolean>>
refresh: () => Promise<void>
}
function useFetch<T>(url: string | Ref<string>): UseFetchReturn<T> {
// ...实现
}
// 使用时自动推断T类型
const { data } = useFetch<User[]>('/api/users')
// data类型为 Readonly<Ref<User[] | null>>
Hooks设计的好坏直接决定项目长期的可维护性。花时间把基础Hooks设计好——返回值稳定、副作用可清理、参数响应式、类型完整——组合出来的业务Hooks才能简洁可靠。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zi-ding-yi-hooks-she-ji-mo-shi-yu-zu-he-shi-api-xing/