Vue 组合式函数设计模式:如何编写高复用性的 composable

组合式函数(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
}

调用方可以传入 refcomputed 或 getter 函数,组合式函数内部通过 watch 自动追踪依赖。这种"接收响应式源"的约定,是 Vue 生态中 composable 的通用惯例,watchcomputed 本身也是这么设计的。

模式二:返回对象而非数组,保留扩展空间

返回值的形态直接影响未来的可维护性。对比两种写法:

// 数组返回
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(如 createRouterdefineStore)普遍采用的风格。

模式四:生命周期与清理逻辑内聚

composable 中如果注册了事件监听、定时器、WebSocket 等资源,必须在组件卸载时清理。关键在于把"注册"和"清理"写在同一个 composable 内部,而不是把清理责任推给调用者:

import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, handler) {
  onMounted(() => target.addEventListener(event, handler))
  onUnmounted(() => target.removeEventListener(event, handler))
}

需要注意的是,onMountedonUnmounted 只能在组件 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

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏