介绍
自定义 Hook 就是利用 Vue3 组合式 API(ref/reactive/watch/onMounted 等)封装逻辑的普通函数。
⚠️ 区别:自定义 Hook ≠ 组件,没有模板,只封装逻辑与状态
作用
抽离复用组件逻辑,解决选项式 API 逻辑分散、难以复用的痛点
命名规范
以 use 开头(约定俗成,便于识别),例如 useMouse、useRequest
使用
基于 <script setup> / setup() 组合式 API,只能在setup 执行阶段调用
应用场景(关键)
满足以下条件之一,就应该使用 Hooks:
- 需要多处复用
- 依赖组件生命周期(onMounted /onUnmounted、事件监听、定时器、请求、订阅等副作用清理)
- 函数内部想要直接调用 useXXXStore()(如:Pinia状态管理)
不适合 Hooks
如果,某些功能函数, 仅在当前页面中使用,不复用;或者 不需要生命周期、事件监听等功能;或者不需要在函数内部调用 useXXXStore(),那么只封装成 普通的 .ts 文件 就够用了
规则(必须遵守)
- 只能在
<script setup>内部 或其他自定义 Hook 中调用 - 不能在普通函数、条件判断、循环、异步回调里调用(遵循组合式API调用顺序规则)
// ❌ 错误示例 function fetchData() { const { data } = useRequest() // 禁止在普通函数内部调用hook } // ✅ 正确 const { data } = useRequest() function fetchData() {} - Hook 内部可以使用所有组合 API:
ref、reactive、computed、watch、生命周期 - 通过
return向外暴露状态和方法;想要外部修改响应式,直接返回响应式变量
目录结构
项目统一存放 hooks,推荐目录:
src/
└── hooks(或composables)/
├── useCounter.js
├── useWindowSize.js
├── useRequest.js # 请求封装
├── useStorage.js # localStorage
└── index.js # 统一导出
index.js 统一导出:
export * from './useCounter'
export * from './useWindowSize'
案例
实现简单计数器 Hook
创建hooks文件
创建文件:hooks/useCounter.js,内容如下:
import { ref } from 'vue'
// 自定义Hook
export function useCounter(initialValue = 0) {
// 内部状态(封装私有数据)
const count = ref(initialValue)
// 内部方法
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
// 向外暴露(响应式数据直接返回)
return {
count,
increment,
decrement,
reset
}
}
使用hooks
创建 view,内容如下:
<script setup>
import { useCounter } from '@/hooks/useCounter'
// 解构拿到响应式变量和方法
const { count, increment, decrement, reset } = useCounter(10)
</script>
<template>
<p>{{ count }}</p>
<button @click="increment">+</button>
</template>
响应式传递要点
1. 返回 ref / reactive → 外部具备响应式(推荐)
export function useDemo() {
const name = ref('张三')
return { name }
}
组件中修改 name.value 会双向生效。
2. 如果直接返回普通值 → 丢失响应式
// ❌ 错误
return {
name: name.value
}
3. 限制外部修改:封装只读状态
使用 readonly 保护内部状态,只开放方法修改
import { ref, readonly } from 'vue'
export function useCounter() {
const count = ref(0)
const add = () => count.value++
return {
count: readonly(count), // 外部不能直接修改count.value
add
}
}
Hook 中使用生命周期、watch
示例:监听窗口大小 useWindowSize
import { ref, onMounted, onUnmounted, watch } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const update = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', update)
})
onUnmounted(() => {
window.removeEventListener('resize', update)
})
return { width, height }
}
关键优势:自动生命周期销毁,组件卸载自动清除事件,不用每个组件重复写解绑。
参数传递:支持传入响应式参数
Hook 可以接收 ref 响应式参数,内部可用 watch 监听
import { ref, watch } from 'vue'
export function useDouble(val) {
const doubleVal = ref(val.value * 2)
watch(val, (v) => {
doubleVal.value = v * 2
}, { immediate: true })
return { doubleVal }
}
组件调用:
const num = ref(10)
const { doubleVal } = useDouble(num)
num.value = 20 // 自动触发hook内部watch更新
组合多个 Hook(Hook 互相调用)
Hook 可以引入其他 Hook,实现逻辑分层
// useMouse.js
export function useMouse() {
const x = ref(0)
const y = ref(0)
// 监听鼠标移动...
return { x, y }
}
// useMouseDistance.js
import { useMouse } from './useMouse'
import { computed } from 'vue'
export function useMouseDistance() {
const { x, y } = useMouse()
const distance = computed(() => {
return Math.sqrt(x.value ** 2 + y.value ** 2)
})
return { distance, x, y }
}
常见实战场景 Hook 模板
1. useStorage 本地存储
import { ref, watch } from 'vue'
export function useStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
watch(data, (val) => {
localStorage.setItem(key, JSON.stringify(val))
}, { deep: true })
return data
}
2. useRequest 简易请求封装
import { ref } from 'vue'
export function useRequest(apiFn) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const run = async (...args) => {
loading.value = true
error.value = null
try {
data.value = await apiFn(...args)
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
return { data, loading, error, run }
}
常见坑与注意事项
- 不要在条件/异步里调用 Hook
// ❌ if (flag.value) { const { count } = useCounter() } 多个组件同时调用同一个 Hook,状态相互隔离
每次调用useCounter()都会创建全新的 ref,组件之间互不干扰;
如果想要全局共享状态,要配合pinia或者单独导出一个外部 ref。Hook 不是单例!
const a = useCounter() const b = useCounter() // a.count 和 b.count 相互独立全局共享逻辑方案
- 组件内复用逻辑 → 自定义Hook
- 跨组件共享状态 → Pinia + Hook
<script setup>与 setup() 选项均可使用自定义Hook// 选项式setup写法也支持 setup() { const { count } = useCounter() return { count } }
自定义Hook vs Mixin 对比
| 特性 | 自定义Hook | Mixin |
|---|---|---|
| 命名冲突 | 无,自由解构 | 容易命名冲突 |
| 数据来源清晰 | 一眼看出来自哪个hook | 来源模糊 |
| 按需引入 | 按需导入 | 全部合并 |
| TS类型支持 | 完美支持 | 类型体验差 |
Vue3 推荐全部使用自定义Hook替代Mixin