在 Vue 3 中,自定义 Hooks(官方称为 Composables,组合式函数)是利用组合式 API(Composition API)来封装和复用有状态逻辑的最佳实践。
它解决了以前 Mixins 存在的命名冲突、来源不清晰以及逻辑分散等痛点。我们可以把一个复杂的组件逻辑拆分成一个个独立的、可测试的微小单元。
1. 基本结构
一个标准的 Composable 通常遵循以下约定:
-
命名: 以
use开头(如useMouse,useFetch)。 -
输入: 可以接收参数(可以是普通值、Ref 或 Getter)。
-
输出: 通常返回一个包含
ref或function的对象(方便解构,但也需注意解构后会丢失响应性的坑,通常建议返回 Ref 对象)。
2. 核心示例:追踪鼠标位置
这是最经典的一个例子,展示了如何封装状态和生命周期钩子。
编写 useMouse.js
JavaScript
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
// 1. 定义内部状态
const x = ref(0)
const y = ref(0)
// 2. 定义改变状态的逻辑
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 3. 挂载生命周期钩子
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 4. 暴露状态(返回对象)
return { x, y }
}
在组件中使用
代码段
<script setup>
import { useMouse } from './useMouse'
// 解构获取响应性数据
const { x, y } = useMouse()
</script>
<template>
<div>Mouse position is at: {{ x }}, {{ y }}</div>
</template>
3. 进阶技巧与最佳实践
为了写出高质量的 Composables,建议关注以下几个维度:
A. 灵活的输入参数
优秀的 Hooks 应该既支持传入普通值,也支持传入 Ref。可以使用 toValue(Vue 3.3+)来统一处理。
JavaScript
import { toValue, watchEffect } from 'vue'
export function useData(url) {
watchEffect(() => {
// 无论 url 是字符串还是 ref,toValue 都能拿到具体的值
const currentUrl = toValue(url)
// 执行 fetch 逻辑...
})
}
B. 始终返回对象而非数组
虽然 React Hooks 习惯返回数组(如 const [val, setVal] = useState()),但在 Vue 中推荐返回对象。这样在解构时可以按需取值,且不需要严格遵守顺序。
C. 资源清理
如果你的 Hook 开启了定时器、网络请求或 DOM 事件监听,务必在 onUnmounted 中清理,防止内存泄漏。
D. 嵌套使用
Composables 内部可以调用其他的 Composables。这种“套娃”能力使得你可以通过组合小的逻辑单元来构建极其复杂的业务逻辑。
4. 为什么选择 Composables 而不是 Mixins?
如果你在犹豫是否要重构旧代码,下表对比了它们的本质区别:
| 特性 | Mixins | Composables (Hooks) |
|---|---|---|
| 属性来源 | 隐式(不知道属性来自哪个 Mixin) | 显式(通过 import 和解构明确来源) |
| 命名冲突 | 容易冲突(多个 Mixin 拥有同名变量) | 无冲突(可以在解构时重命名) |
| 逻辑组织 | 碎片化(按生命周期拆分) | 集中式(将同一功能的逻辑聚拢在一起) |
| 类型支持 | 较差 | 优秀(原生支持 TypeScript) |
我的建议
在实际开发中,不要为了拆分而拆分。如果逻辑只在一个组件中使用且代码量适中,直接写在组件内即可。当你发现某个逻辑(如:分页处理、表单校验、防抖节流、第三方库初始化)在多个地方出现时,才是抽象成 Composable 的最佳时机。
此外,在编写涉及异步请求的 Hooks 时,建议利用 Vue 的异步组件和 Suspense 特性,或者像 VueUse 库那样返回 isLoading 和 error 状态。
Vue Composables 和 React Hooks 的区别
虽然 Vue 3 的 Composables 在命名上致敬了 React Hooks(都以 use 开头),但如果你拆开“引擎盖”看一眼,会发现它们的工作原理其实是两种完全不同的哲学。
简单来说:React Hooks 是“快照”,而 Vue Composables 是“连接”。
1. 执行机制:一次性 vs. 反复运行
这是两者最本质的区别,决定了你编写代码时的心智负担。
-
React Hooks: 每当组件状态改变,整个函数组件会重新执行。这意味着所有的 Hooks 也会跟着重新运行。为了防止性能问题或无限循环,你必须小心翼翼地使用
useMemo、useCallback和依赖数组。 -
Vue Composables:
setup()(或<script setup>)只在组件创建时执行一次。它建立了一套响应式的追踪系统。后续的更新是由数据直接触发对应的 DOM 局部变更,而不是重新运行整个逻辑块。
2. 闭包与“心智负担”
如果你写过 React,一定被“过时闭包(Stale Closures)”坑过。
-
React 的困境: 由于函数反复执行,如果你在某个异步回调里访问状态,拿到的可能是“旧帧”里的数据。
-
Vue 的优势: 因为代码只运行一次,且数据是基于 Proxy 的引用,你拿到的永远是那个响应式对象的最新值。你不需要担心闭包捕获了旧变量,因为“变量”本身就是响应式的容器。
3. 调用规则的约束
React 有著名的“Hooks 规则”,而 Vue 几乎没有。
| 场景 | React Hooks | Vue Composables |
|---|---|---|
| 循环/条件判断 | 严禁使用。Hooks 必须严格按照固定的顺序调用。 | 允许使用。因为逻辑只在初始化时运行一次,不依赖调用顺序。 |
| 手动依赖管理 | 必须在 useEffect 等后方手动维护 [dep] 数组。 |
自动追踪。watch 和 computed 会自动收集依赖。 |
| 性能优化 | 需要手动 useCallback 避免子组件无谓重绘。 |
自动优化。细粒度的响应式系统确保只有受影响的部分更新。 |
4. 数据模型的对比
-
React 是不可变(Immutable)的: 更新状态必须给出一个全新的对象(
setState(prev => ({...prev, count: 1})))。 -
Vue 是可变(Mutable)的: 你直接修改
count.value++即可。Vue 的响应式引擎会自动感知并通知订阅者。
5. 直观对比表
| 特性 | React Hooks | Vue 3 Composables |
|---|---|---|
| 底层原理 | 闭包 + 链表存储 | Proxy + 依赖收集 |
| 执行频率 | 每次渲染都执行 | 仅在初始化时执行一次 |
| 依赖声明 | 必须手动声明依赖数组 | 自动收集依赖,零手动维护 |
| 学习曲线 | 初期简单,进阶处理闭包/性能极难 | 概念稍多(ref, reactive),但后期极稳 |
| 代码组织 | 逻辑容易散落在各处 | 逻辑高度内聚,非常利于复用 |
总结建议
如果你从 React 转到 Vue,最需要改变的习惯就是:别再纠结“依赖数组”了。
在 React 中,你是在编写一段“描述当前状态的代码”;而在 Vue 中,你是在编写一个“生产响应式数据的工厂”。
Vue 的 Composables 让你能像写普通 JavaScript 逻辑一样去组织代码,而不用担心因为少写了一个依赖项导致程序出现诡异的 Bug。
你想深入了解一下 Vue 是如何通过 watchEffect 实现比 React useEffect 更智能的自动依赖追踪的吗?