Vue3自定义Hooks设计模式与组合式API性能优化实战

组合式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/

(0)
小编小编
上一篇 14小时前
下一篇 14小时前

相关推荐