在 Vue3 中,组合式 API(Composition API)让逻辑复用变得更加自然。而 自定义 Hook(组合式函数) 正是其中的核心:它们像 React Hook 一样,把逻辑抽离成独立函数,在多个组件中复用。

Hook 离我们并不远,而且一点也不复杂。本文将从浅到深理解自定义 Hook,并通过四个层次的案例,以及通用 Hook 模板,让你快速掌握它的用法。
👉 花 2 分钟安静阅读,相信我,这篇文章一定能带来启发。~~
为什么需要自定义 Hook?
在 Vue 2.x 的 Options API 中,逻辑往往分散在 data
、methods
、watch
等配置项里。想要复用时只能依赖 Mixin,但 Mixin 存在不少问题:
- 来源不清晰,变量和方法从哪来?需要翻文件。
- 多个 Mixin 混用时容易冲突,逻辑也难追踪。
- 对 TypeScript 不友好,类型推导混乱。
来看一个例子:
Vue2.x Mixin 写法
jsx
// counterMixin.js
export const counterMixin = {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
html
<template>
<div>
<p>count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { counterMixin } from './counterMixin'
export default {
mixins: [counterMixin]
}
</script>
这里你可能会疑惑:count
和 increment
从哪里来的?要去看 mixin 文件才能搞明白。
Vue3 Hook 写法
jsx
// useCounter.ts
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
jsx
<script setup lang="ts">
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>
<template>
<div>
<p>count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
相比之下:
useCounter
一看就知道逻辑来源。- 不会命名冲突。
- 对 TypeScript 友好,类型推导清晰。
下面,我们就通过 四个层次的案例,逐步感受 Hook 的魅力。
四个层次的案例

一、最简单的 Hook:鼠标跟随
比如我们想要在多个组件中实现 鼠标跟随效果。
jsx
// useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
在组件中使用:
jsx
<script setup lang="ts">
import { useMouse } from '@/hooks/useMouse'
const { x, y } = useMouse()
</script>
<template>
<div>鼠标位置:{{ x }} , {{ y }}</div>
</template>
这样,一个简单的逻辑就能在多个地方复用。
二、带参数的 Hook:倒计时
有时候 Hook 需要支持配置,比如 倒计时:
jsx
// useCountdown.ts
import { ref, onUnmounted } from 'vue'
export function useCountdown(initial: number = 60, interval = 1000) {
const time = ref(initial)
let timer: number | null = null
function start() {
if (timer) return
timer = setInterval(() => {
if (time.value > 0) {
time.value--
} else {
stop()
}
}, interval) as unknown as number
}
function stop() {
if (timer) {
clearInterval(timer)
timer = null
}
}
onUnmounted(stop)
return { time, start, stop }
}
使用:
jsx
<script setup lang="ts">
import { useCountdown } from '@/hooks/useCountdown'
const { time, start, stop } = useCountdown(10)
</script>
<template>
<div>
倒计时:{{ time }}
<button @click="start">开始</button>
<button @click="stop">停止</button>
</div>
</template>
👉 这就是 参数化能力:更灵活、更通用。
三、结合外部依赖:监听窗口大小
Hook
也可以对接外部依赖库。
比如监听 窗口大小,在图表自适应、响应式布局中很常用:
jsx
// useWindowSize.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
return { width, height }
}
这类 Hook 常见于 UI 框架,我们也能轻松实现。
四、进阶:组合 Hook
Hook 本身也能 组合 ,像乐高一样拼接逻辑。
比如结合 useMouse
和 useWindowSize
,计算 鼠标相对窗口的百分比位置:
jsx
// useMousePercent.ts
import { computed } from 'vue'
import { useMouse } from './useMouse'
import { useWindowSize } from './useWindowSize'
export function useMousePercent() {
const { x, y } = useMouse()
const { width, height } = useWindowSize()
const percentX = computed(() => (x.value / width.value) * 100)
const percentY = computed(() => (y.value / height.value) * 100)
return { percentX, percentY }
}
👉 这就是组合的威力:小 Hook 拼成大逻辑。
通用 Hook 模板(开箱即用)
讲到这里,相信你已经大概掌握了写 Hook 的套路。🙌
那问题来了:真正写的时候从哪下手?
别担心,我准备了一个 万能 Hook 模板,直接拷贝就能跑,改一改就是你的专属逻辑~
jsx
// useTemplate.ts
import { ref, onMounted, onUnmounted } from 'vue'
/**
* 通用 Hook 模板
* @param options 可选参数配置
*/
export function useTemplate(options?: { initial?: number }) {
// 1. 定义状态
const state = ref(options?.initial ?? 0)
// 2. 定义逻辑方法
function start() {
console.log('start...')
}
function stop() {
console.log('stop...')
}
// 3. 生命周期绑定
onMounted(() => {
console.log('Hook 已挂载')
})
onUnmounted(() => {
console.log('Hook 已卸载')
})
// 4. 返回值(状态 + 方法)
return {
state,
start,
stop,
}
}
使用方式:
html
<script setup lang="ts">
import { useTemplate } from '@/hooks/useTemplate'
const { state, start, stop } = useTemplate({ initial: 10 })
</script>
<template>
<div>
<p>当前状态:{{ state }}</p>
<button @click="start">开始</button>
<button @click="stop">停止</button>
</div>
</template>
"你会怎么改造这个模板?欢迎在评论区分享你写过的 Hook!"
最佳实践与注意事项
- 命名规范 :一般以
useXxx
命名,直观易懂。保持函数职责单一,避免过度臃肿。 - 与组件生命周期解耦 :Hook 内部可使用
onMounted
/onUnmounted
,确保资源正确释放。 - 可组合性优先:小而精的 Hook 更容易被组合和复用。
- 工具库推荐 :社区已有大量高质量 Hook,如
VueUse
,覆盖了些常见场景(网络、DOM、状态管理等),能复用就别重复造轮子。
总结
自定义 Hook 是 Vue3 中逻辑复用的核心方式。
从最简单的逻辑抽离,到参数化,再到组合与工具化,都体现了它的灵活性和可维护性。实际项目里,Hook 不仅能让代码更清晰,还能逐渐沉淀出 逻辑库。
希望对你有所帮助、有所借鉴。你有什么想法或者问题,欢迎在评论区一起讨论。