×

Vue3 自定义 Hooks 实战:10 个高频场景

独孤求败 独孤求败 发表于2026-06-17 16:06:36 浏览37 评论0

抢沙发发表评论

Vue 3 的组合式 API 让逻辑复用变得前所未有的简单,自定义 Hooks 是其中的核心手段。本文整理 10 个高频场景的 Hooks 实现,可直接复制到项目中使用。


一、useRequest:请求封装

封装 loading、error、data 的请求逻辑,支持手动和自动触发。

// composables/useRequest.ts
import
 { ref, shallowRef } from 'vue'

export
 function useRequest<T>(fetcher: () => Promise<T>, options?: { immediate?: boolean }) {
  const
 data = shallowRef<T | undefined>()
  const
 loading = ref(false)
  const
 error = ref<Error | null>(null)

  const
 run = async () => {
    loading.value = true
    error.value = null
    try
 {
      data.value = await fetcher()
      return
 data.value
    } catch (e) {
      error.value = e as Error
      throw
 e
    } finally {
      loading.value = false
    }
  }

  if
 (options?.immediate !== false) {
    run
()
  }

  return
 { data, loading, error, run }
}

使用示例:

<script setup>
const { data, loading, error, run } = useRequest(() =>
  fetch('/api/user').then((r) => r.json())
)
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <div v-else>{{ data }}</div>
</template>

二、useDebounce:防抖

// composables/useDebounce.ts
import
 { ref, watch } from 'vue'

export
 function useDebounce<T>(value: () => T, delay = 300) {
  const
 debounced = ref(value()) as { value: T }
  let
 timer: ReturnType<typeof setTimeout>

  watch
(value, (v) => {
    clearTimeout
(timer)
    timer = setTimeout(() => {
      debounced.value = v
    }, delay)
  }, { immediate: true })

  return
 debounced
}

更常用的写法是防抖函数:

export function useDebounceFn<T extends (...args: unknown[]) => unknown>(
  fn
: T,
  delay = 300
) {
  let
 timer: ReturnType<typeof setTimeout>
  return
 (...args: Parameters<T>) => {
    clearTimeout
(timer)
    timer = setTimeout(() => fn(...args), delay)
  }
}

三、useThrottle:节流

// composables/useThrottle.ts
export
 function useThrottleFn<T extends (...args: unknown[]) => unknown>(
  fn
: T,
  interval = 300
) {
  let
 last = 0
  return
 (...args: Parameters<T>) => {
    const
 now = Date.now()
    if
 (now - last >= interval) {
      last = now
      fn
(...args)
    }
  }
}

四、useLocalStorage:持久化存储

// composables/useLocalStorage.ts
import
 { ref, watch } from 'vue'

export
 function useLocalStorage<T>(key: string, defaultValue: T) {
  const
 data = ref<T>(
    (() => {
      try
 {
        const
 raw = localStorage.getItem(key)
        return
 raw ? JSON.parse(raw) : defaultValue
      } catch {
        return
 defaultValue
      }
    })()
  ) as { value: T }

  watch
(data, (v) => {
    localStorage
.setItem(key, JSON.stringify(v))
  }, { deep: true })

  return
 data
}

五、useClickOutside:点击外部关闭

// composables/useClickOutside.ts
import
 { onMounted, onUnmounted } from 'vue'

export
 function useClickOutside(
  target
: () => HTMLElement | null,
  callback
: () => void
) {
  const
 handler = (e: MouseEvent) => {
    const
 el = target()
    if
 (el && !el.contains(e.target as Node)) {
      callback
()
    }
  }

  onMounted
(() => document.addEventListener('click', handler))
  onUnmounted
(() => document.removeEventListener('click', handler))
}
<script setup>
const dropdownRef = ref(null)
useClickOutside(() => dropdownRef.value, () => (showMenu.value = false))
</script>

六、useInfiniteScroll:无限滚动

// composables/useInfiniteScroll.ts
import
 { ref, onMounted, onUnmounted } from 'vue'

export
 function useInfiniteScroll(
  loadMore
: () => Promise<void>,
  options
?: { root?: Element | null; threshold?: number }
) {
  const
 loading = ref(false)
  const
 sentinel = ref<HTMLElement | null>(null)

  const
 observer = new IntersectionObserver(
    async
 (entries) => {
      if
 (loading.value || !entries[0]?.isIntersecting) return
      loading.value = true
      try
 {
        await
 loadMore()
      } finally {
        loading.value = false
      }
    },
    { root: options?.root ?? null, threshold: options?.threshold ?? 0 }
  )

  onMounted
(() => {
    if
 (sentinel.value) observer.observe(sentinel.value)
  })
  onUnmounted
(() => observer.disconnect())

  return
 { sentinel, loading }
}

七、useClipboard:剪贴板

// composables/useClipboard.ts
import
 { ref } from 'vue'

export
 function useClipboard() {
  const
 copied = ref(false)

  const
 copy = async (text: string) => {
    try
 {
      await
 navigator.clipboard.writeText(text)
      copied.value = true
      setTimeout
(() => (copied.value = false), 2000)
      return
 true
    } catch {
      return
 false
    }
  }

  return
 { copied, copy }
}

八、useDarkMode:暗色模式

// composables/useDarkMode.ts
import
 { ref, watch } from 'vue'

export
 function useDarkMode() {
  const
 isDark = ref(
    typeof
 document !== 'undefined' &&
      document
.documentElement.classList.contains('dark')
  )

  watch
(isDark, (v) => {
    document
.documentElement.classList.toggle('dark', v)
    localStorage
.setItem('theme', v ? 'dark' : 'light')
  }, { immediate: true })

  const
 toggle = () => (isDark.value = !isDark.value)

  return
 { isDark, toggle }
}

九、useCounter:计数器

// composables/useCounter.ts
import
 { ref, computed } from 'vue'

export
 function useCounter(initial = 0, options?: { min?: number; max?: number }) {
  const
 count = ref(initial)

  const
 inc = () => {
    if
 (options?.max !== undefined && count.value >= options.max) return
    count.value++
  }

  const
 dec = () => {
    if
 (options?.min !== undefined && count.value <= options.min) return
    count.value--
  }

  const
 reset = () => (count.value = initial)

  return
 {
    count
: computed(() => count.value),
    inc,
    dec,
    reset,
  }
}

十、useForm:表单校验封装

// composables/useForm.ts
import
 { reactive, toRefs } from 'vue'

export
 function useForm<T extends Record<string, unknown>>(
  initial
: T,
  validate
?: (values: T) => Record<keyof T, string | undefined>
) {
  const
 form = reactive({ ...initial }) as T
  const
 errors = reactive({} as Record<keyof T, string>) as Record<keyof T, string>

  const
 validateForm = () => {
    if
 (!validate) return true
    const
 res = validate(form)
    let
 valid = true
    for
 (const k of Object.keys(res) as (keyof T)[]) {
      errors[k] = res[k] ?? ''
      if
 (res[k]) valid = false
    }
    return
 valid
  }

  const
 reset = () => {
    Object
.assign(form, initial)
    Object
.keys(errors).forEach((k) => (errors[k as keyof T] = ''))
  }

  return
 { ...toRefs(form), errors, validateForm, reset }
}

总结

Hook
场景
useRequest
异步请求
useDebounce
防抖
useThrottle
节流
useLocalStorage
持久化
useClickOutside
点击外部
useInfiniteScroll
无限滚动
useClipboard
剪贴板
useDarkMode
暗色模式
useCounter
计数器
useForm
表单校验

自定义 Hooks 的核心是单一职责可组合。把业务逻辑从组件中抽离,代码更清晰、复用性更强。


群贤毕至

访客