组合式函数(Composable)是 Vue 3 组合式 API 最强大的能力之一。它让我们能够像搭积木一样,把有状态的逻辑封装成可复用的单元。然而,写一个"能用"的 composable 和写一个"高复用"的 composable,中间隔着不少设计上的思考。本文将从实践角度出发,梳理几种经过验证的设计模式,帮助你写出真正经得起项目考验的组合式函数。
什么是高复用性的 composable
先明确一个判断标准。一个高复用性的 composable 通常具备以下特征:
- 职责单一:只解决一个明确的问题,不掺杂无关逻辑
- 接口清晰:输入参数和返回值语义明确,调用者不需要阅读源码就能正确使用
- 副作用可控:对响应式状态、生命周期、DOM 等副作用的处理是可预期的
- 环境无关:不硬编码具体的 API 地址、存储键名或业务常量
- 可组合:能与其他 composable 嵌套使用,而不是一个封闭的黑盒
下面逐一展开对应的设计模式。
模式一:以 ref 为输入,保持响应式连接
很多初学者写的 composable 接收普通值作为参数,这会导致响应式丢失。来看一个典型场景——封装一个防抖函数:
// 有问题的写法:接收普通值
function useDebouncedValue(value, delay) {
const debounced = ref(value)
// ...
return debounced
}
当 value 是一个 ref 时,传入的是 ref 对象本身而非其值,逻辑会出错;当外部值变化时,内部也无法感知。正确的做法是统一接收 ref 或 getter:
import { ref, watch } from 'vue'
export function useDebouncedValue(source, delay = 300) {
const debounced = ref()
watch(source, (val) => {
const timer = setTimeout(() => {
debounced.value = val
}, delay)
// 清理逻辑
return () => clearTimeout(timer)
}, { immediate: true })
return debounced
}
调用方可以传入 ref、computed 或 getter 函数,组合式函数内部通过 watch 自动追踪依赖。这种"接收响应式源"的约定,是 Vue 生态中 composable 的通用惯例,watch、computed 本身也是这么设计的。
模式二:返回对象而非数组,保留扩展空间
返回值的形态直接影响未来的可维护性。对比两种写法:
// 数组返回
return [count, increment]
// 对象返回
return { count, increment }
数组返回在解构时需要严格对应顺序,一旦后续想增加一个返回值,要么追加到末尾(调用方不感知),要么破坏已有调用。对象返回则允许按需解构,新增字段完全向后兼容:
const { count, increment } = useCounter()
此外,对象返回还能配合 TypeScript 提供精确的类型提示。除非是像 useFetch 那样语义上天然成对的值(如 [error, data]),否则优先返回对象。
模式三:用选项对象替代长参数列表
当 composable 需要多个配置项时,参数列表会迅速膨胀:
useRequest(url, 'GET', true, 5000, 3, handleError)
调用者根本记不住第三个参数是什么。改用选项对象:
useRequest(url, {
method: 'GET',
immediate: true,
timeout: 5000,
retry: 3,
onError: handleError
})
配合默认值合并,每个选项都是可选的,调用者只需关心自己需要的部分。这也是 Vue 官方 API(如 createRouter、defineStore)普遍采用的风格。
模式四:生命周期与清理逻辑内聚
composable 中如果注册了事件监听、定时器、WebSocket 等资源,必须在组件卸载时清理。关键在于把"注册"和"清理"写在同一个 composable 内部,而不是把清理责任推给调用者:
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, handler) {
onMounted(() => target.addEventListener(event, handler))
onUnmounted(() => target.removeEventListener(event, handler))
}
需要注意的是,onMounted、onUnmounted 只能在组件 setup 期间同步调用。如果 composable 内部有异步逻辑,生命周期钩子的注册必须放在同步代码路径中,否则会失效。这是实践中很容易踩的坑。
模式五:用工厂函数隔离状态
一个常见的误解是:composable 每次调用都会创建独立状态。实际上,如果状态定义在模块顶层,所有调用者会共享同一份数据:
// 模块级状态:全局共享
const count = ref(0)
export function useCounter() {
return { count }
}
这有时是想要的(比如全局主题、用户信息),但更多时候我们希望每次调用都有独立状态。解决办法是把状态定义在函数内部:
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
return { count, increment }
}
如果确实需要共享状态(如全局单例),可以显式使用工厂 + 模块级缓存,并在命名上体现出来,比如 useGlobalStore,让调用者心里有数。
模式六:组合优于继承,嵌套组合式函数
高复用性的 composable 往往不是从零构建的,而是由更小的 composable 组合而成。例如封装一个表单校验:
export function useFormField(initialValue, rules) {
const value = ref(initialValue)
const error = ref('')
const { debounced } = useDebounce(value, 300)
const { validate } = useValidation(rules)
watch(debounced, async (val) => {
error.value = await validate(val)
})
return { value, error }
}
每个子 composable 各司其职,useFormField 只负责编排。这种分层让每一层都能被单独测试和复用,也避免了单个函数膨胀成几百行的"上帝函数"。
小结
编写高复用性的 Vue composable,核心不在于技巧有多花哨,而在于几个朴素的约定:接收响应式源、返回对象、用选项对象做配置、内聚生命周期、按需隔离状态、通过组合构建复杂逻辑。这些模式并非孤立存在,实际项目中往往是组合使用。
最后给一个实用建议:在写 composable 之前,先问自己"如果换一个项目,这个函数还能直接用吗?"如果答案是否定的,说明里面还掺杂了业务耦合,值得再抽一层。好的 composable 应该像标准库函数一样,让人不需要看实现就能放心使用。
未经允许不得转载:任鹏个人博客 » Vue 组合式函数设计模式:如何编写高复用性的 composable


朋友圈点赞图在线生成源码