🚀Vue3 Hooks封装实战:告别重复代码,写出高复用、高质量的自定义Hook

在Vue3 Composition API时代,自定义Hook(简称Composables)是提升代码复用性、解耦业务逻辑的核心利器。但很多开发者用Hook时要么写成“伪代码”(逻辑混乱、复用性差),要么只会简单封装,没发挥出Hook的真正价值。

今天就带大家彻底搞懂Vue3 Hooks封装:从基础实现、使用方法,到高质量封装的核心原则,手把手教你写出“干净、可复用、易维护”的自定义Hook,告别重复造轮子!

📌 先搞懂:什么是Vue3自定义Hook?

Hook本质是抽离出来的、可复用的组合式函数,以useXXX命名(约定俗成),把组件中通用的逻辑(如请求、防抖、监听、状态管理)封装成独立函数,让组件只专注于渲染逻辑。

✅ 核心优势:

  • 复用性:一处封装,多处调用,告别复制粘贴;

  • 解耦:把业务逻辑从组件中抽离,组件代码更简洁;

  • 可维护:逻辑集中管理,修改只需改Hook,无需改所有使用的组件;

  • 可测试:独立函数更容易编写单元测试。

🛠 基础篇:从零实现第一个自定义Hook

先从最简单的场景入手,比如封装一个“防抖Hook”(开发中高频使用),理解Hook的基本实现和使用方式。

1. 目录规范(第一步:做好结构)

首先规划Hook的目录结构,避免零散存放:

src/
├── hooks/                # 所有自定义Hook统一存放
│   ├── index.js          # Hook导出入口(按需聚合)
│   ├── useDebounce.js    # 防抖Hook
│   ├── useRequest.js     # 请求Hook(后续实战)
│   └── useEventListener.js # 事件监听Hook
└── components/           # 组件目录(使用Hook)

2. 实现第一个Hook:useDebounce

需求:封装防抖逻辑,传入函数和延迟时间,返回防抖后的函数。

  // src/hooks/useDebounce.js
import { ref } from 'vue'

/**
 * 防抖Hook
 * @param {Function} fn - 需要防抖的函数
 * @param {number} delay - 防抖延迟时间(默认300ms)
 * @returns {Function} 防抖后的函数
 */
export function useDebounce(fn, delay = 300) {
  // 定义定时器ref(响应式,避免丢失)
  const timer = ref(null)

  // 封装防抖函数
  const debouncedFn = (...args) => {
    // 清除上一次定时器
    if (timer.value) clearTimeout(timer.value)
    // 重新设置定时器
    timer.value = setTimeout(() => {
      // 执行原函数,绑定this并传递参数
      fn.apply(this, args)
    }, delay)
  }

  // 提供清除定时器的方法(避免内存泄漏)
  const cancel = () => {
    if (timer.value) clearTimeout(timer.value)
    timer.value = null
  }

  // 返回防抖函数和取消方法
  return { debouncedFn, cancel }
}

3. 在组件中使用useDebounce

<!-- src/components/SearchInput.vue -->
<template>
  <input v-model="searchValue" @input="handleInput" placeholder="请输入搜索内容" />
</template>

<script setup>
import { ref } from 'vue'
import { useDebounce } from '@/hooks/useDebounce'

const searchValue = ref('')

// 定义原始搜索函数
const search = (value) => {
  console.log('发起搜索:', value)
  // 实际开发中这里调用接口
}

// 使用防抖Hook,延迟500ms
const { debouncedFn: debouncedSearch } = useDebounce((value) => {
  search(value)
}, 500)

// 输入事件绑定防抖函数
const handleInput = (e) => {
  searchValue.value = e.target.value
  debouncedSearch(e.target.value)
}
</script>

�进阶篇:封装高频实战Hook——useRequest

防抖只是基础,接下来封装开发中最常用的“请求Hook”,覆盖加载状态、错误处理、取消请求等核心场景,体现高质量Hook的封装思路。

1. 需求分析(先明确要解决的问题)

一个高质量的请求Hook需要包含:

  • 加载状态(loading):用于页面loading展示;

  • 数据返回(data):请求成功的结果;

  • 错误信息(error):请求失败的提示;

  • 主动触发(run):手动调用请求;

  • 取消请求(cancel):避免无用请求;

  • 刷新(refresh):重新发起请求;

  • 防抖/节流(可选):防止重复请求。

2. 实现useRequest Hook

// src/hooks/useRequest.js
import { ref, unref, onUnmounted } from 'vue'

/**
 * 通用请求Hook
 * @param {Function} requestFn - 异步请求函数(需返回Promise)
 * @param {Object} options - 配置项
 * @param {boolean} options.manual - 是否手动触发(默认false,初始化自动执行)
 * @param {number} options.debounceDelay - 防抖延迟(0表示不防抖)
 * @param {Function} options.onSuccess - 请求成功回调
 * @param {Function} options.onError - 请求失败回调
 * @returns {Object} { loading, data, error, run, cancel, refresh }
 */
export function useRequest(requestFn, options = {}) {
  // 默认配置
  const {
    manual = false,
    debounceDelay = 0,
    onSuccess = () => {},
    onError = () => {}
  } = options

  // 响应式状态
  const loading = ref(false) // 加载状态
  const data = ref(null)     // 请求结果
  const error = ref(null)    // 错误信息
  let abortController = null // 用于取消fetch请求(axios用cancelToken)
  let debounceTimer = null   // 防抖定时器

  // 取消请求方法
  const cancel = () => {
    if (abortController) abortController.abort()
    loading.value = false
    error.value = null
    if (debounceTimer) clearTimeout(debounceTimer)
  }

  // 核心请求逻辑
  const run = async (...args) => {
    // 防抖处理
    if (debounceDelay > 0) {
      if (debounceTimer) clearTimeout(debounceTimer)
      return new Promise((resolve) => {
        debounceTimer = setTimeout(async () => {
          const res = await executeRequest(...args)
          resolve(res)
        }, debounceDelay)
      })
    }
    // 无防抖直接执行
    return executeRequest(...args)
  }

  // 实际执行请求的函数
  const executeRequest = async (...args) => {
    // 重置状态
    loading.value = true
    error.value = null
    try {
      // 创建取消控制器(适配fetch)
      abortController = new AbortController()
      // 执行请求函数,传递参数和取消信号
      const result = await requestFn(...args, { signal: abortController.signal })
      data.value = result
      onSuccess(result) // 成功回调
      return result
    } catch (err) {
      // 排除手动取消的情况
      if (err.name !== 'AbortError') {
        error.value = err
        onError(err) // 失败回调
      }
      throw err
    } finally {
      loading.value = false
      abortController = null
    }
  }

  // 刷新请求(复用最后一次参数)
  const refresh = () => run(...lastArgs)
  let lastArgs = [] // 缓存最后一次请求参数

  // 重写run,缓存参数
  const originRun = run
  run = (...args) => {
    lastArgs = args
    return originRun(...args)
  }

  // 组件卸载时取消请求(防止内存泄漏)
  onUnmounted(() => {
    cancel()
  })

  // 非手动模式:初始化自动执行
  if (!manual) {
    run()
  }

  // 返回核心状态和方法
  return {
    loading: unref(loading), // 转为非响应式(可选,根据需求)
    data,
    error,
    run,
    cancel,
    refresh
  }
}

3. 在组件中使用useRequest

<!-- src/components/UserList.vue -->
<template>
  <div>
    <!-- 加载状态 -->
    <div v-if="loading">加载中...</div>
    <!-- 错误提示 -->
    <div v-if="error" class="error">{{ error.message }}</div>
    <!-- 数据展示 -->
    <ul v-if="data">
      <li v-for="user in data" :key="user.id">{{ user.name }}</li>
    </ul>
    <!-- 手动触发/刷新 -->
    <button @click="refresh">刷新列表</button>
    <button @click="cancel">取消请求</button>
  </div>
</template>

<script setup>
import { useRequest } from '@/hooks/useRequest'

// 模拟异步请求函数(实际项目中是axios/fetch请求)
const fetchUserList = async (page = 1) => {
  const res = await fetch(`/api/users?page=${page}`)
  return res.json()
}

// 使用请求Hook
const { loading, data, error, run, cancel, refresh } = useRequest(
  fetchUserList,
  {
    manual: false, // 初始化自动执行
    debounceDelay: 300, // 防抖300ms
    onSuccess: (res) => {
      console.log('用户列表请求成功:', res)
    },
    onError: (err) => {
      console.error('用户列表请求失败:', err)
    }
  }
)

// 手动触发请求(比如分页切换)
const changePage = (page) => {
  run(page)
}
</script>

🎯 高质量Hook封装的核心原则(避坑+提效)

很多开发者写的Hook看似能用,实则是“伪代码”(复用性低、易出bug),遵循以下原则,让你的Hook质量翻倍:

1. 单一职责:一个Hook只做一件事

❌ 错误示例:一个Hook既处理请求,又处理防抖,还处理表单验证;

✅ 正确示例:useRequest只处理请求逻辑,防抖作为可选配置,表单验证单独封装useFormValidate

2. 规范命名:统一useXXX格式

  • 命名必须以use开头,比如useDebounceuseRequestuseEventListener

  • 名称清晰表达功能,避免模糊命名(如不用useUtil,要用useLocalStorage)。

3. 暴露清晰的API:只返回必要的状态/方法

❌ 错误示例:返回所有内部变量(如定时器、临时参数);

✅ 正确示例:只返回组件需要的状态(loading/data)和方法(run/cancel),内部逻辑封装。

4. 处理边界情况:避免“伪代码”陷阱

  • 内存泄漏:组件卸载时清理定时器、取消请求、移除事件监听;

  • 参数校验:对入参做基础校验(如useDebounce检查fn是否为函数);

  • 异常捕获:Hook内部捕获异常,避免组件崩溃;

  • 默认值:给可选参数设置合理默认值,降低使用成本。

5. 响应式兼容:支持ref和原始值

使用unref处理入参,让Hook既支持原始值,也支持响应式ref:

// 示例:兼容ref和原始值
import { unref } from 'vue'

export function useExample(value) {
  // 无论传入的是ref(10)还是10,都能获取到值
  const realValue = unref(value)
  // ...
}

6. 避免耦合:Hook不依赖具体业务

❌ 错误示例:Hook中硬编码接口地址、业务状态;

✅ 正确示例:通过参数传递业务相关内容(如useRequest接收请求函数,不硬编码接口)。

7. 文档注释:让别人能看懂、会使用

每个Hook都要写清晰的注释:

  • 功能描述;

  • 参数说明(类型、默认值、作用);

  • 返回值说明;

  • 使用示例(可选)。

8. 高质量Hook vs 伪代码Hook对比

维度 高质量Hook 伪代码Hook
复用性 多处调用,适配不同场景 仅适配当前组件,无法复用
边界处理 清理定时器、捕获异常 无清理逻辑,易内存泄漏
接口清晰 只暴露必要API 返回所有内部变量,混乱
兼容性 支持ref和原始值 仅支持固定类型参数

📝 常用Hook封装清单(直接复用)

给大家整理了开发中高频使用的自定义Hook,可直接封装使用:

  1. 基础工具类useDebounce(防抖)、useThrottle(节流)、useEventListener(事件监听)、useLocalStorage(本地存储);

  2. 业务通用类useRequest(请求)、useFormValidate(表单验证)、usePagination(分页)、usePermission(权限控制);

  3. 交互类useModal(弹窗管理)、useScroll(滚动监听)、useCopy(复制粘贴)。

🎯 总结

核心要点回顾

  1. Hook本质:以useXXX命名的组合式函数,抽离通用逻辑,提升复用性;

  2. 封装原则:单一职责、暴露清晰API、处理边界情况、避免业务耦合;

  3. 质量关键:清理内存泄漏、兼容响应式、做好参数校验和异常捕获;

  4. 使用技巧:统一目录结构,做好注释,让Hook“即插即用”。

新手避坑提醒

  • ❌ 不要把Hook写成“组件逻辑的复制粘贴”,要抽象通用逻辑;

  • ❌ 不要在Hook中操作DOM(除非专门封装DOM相关Hook);

  • ❌ 不要过度封装:简单逻辑无需强行拆分成Hook,避免增加复杂度。

自定义Hook是Vue3中最能体现“组合式”思想的特性,掌握好Hook封装,能让你的代码从“能用”变成“好用”,大幅提升开发效率和项目可维护性。

如果你有更多Hook封装的实战场景或问题,欢迎在评论区交流~ 后续分享更多Vue3实战技巧!




🚀🚀 往期项目搭建,可前往

往期项目搭建,可前往[ Vue3路由实战:优雅封装+灵活拦截,解锁路由配置新]👆
往期项目搭建,可前往[ vue3 + TS + vite 搭建中后台管理系统(完整项目)]👆
往期项目搭建,可前往[ vue3 + Electron + vite 搭建桌面端应用(完整项目)]👆
往期项目搭建,可前往[ vue3 + vite + ts + element-ui搭建后台管理框架 ]👆


在这里插入图片描述

总结:

前端路上 | 所知甚少,唯善学。
各位小伙伴有什么疑问,欢迎留言探讨。

— 关注我:前端路上不迷路 —




Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐