Vue 3 Composable 函数指南

本文系统阐述 Vue 3 Composable 函数的本质、编写规范与实践要点:强调必须同步调用、返回含 ref 的普通对象以保持响应性、遵循单一职责和 use 前缀命名约定;分析其相较于 Mixins 的优势及与 React Hooks 的关键差异;并说明可测试性策略——区分是否依赖组件实例,指导如何设计高内聚、易测试、可复用的组合式逻辑。

Composable 函数是 Vue 3 Composition API 的核心实践模式。本文从概念、编写、约束到测试,给出一份可以直接落地的完整说明。


一、是什么

Composable 函数是利用 Composition API 封装和复用有状态逻辑的普通 JavaScript 函数,约定以 use 开头命名。

它本质上就是一个函数:内部使用 ref、computed、watch、生命周期钩子等 Composition API,向外返回响应式状态和操作方法。

// useCounter.js
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

使用:

<script setup>
import { useCounter } from './useCounter'
const { count, increment, decrement } = useCounter(10)
</script>

<template>
  <p>{{ count }}</p>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</template>

每个组件调用 useCounter 都会得到独立的响应式状态,互不干扰。


二、为什么需要它

Vue 2 时代用 Mixins 复用逻辑,问题很明确:

问题说明
命名冲突多个 Mixin 的同名属性/方法会互相覆盖,且不报错
来源不透明组件里凭空出现的属性,难以追溯来自哪个 Mixin
逻辑分散一个功能的代码被拆到多个 Mixin 中,改一处要跳多个文件

Composable 通过显式导入、显式解构解决了这些问题:逻辑来源一目了然,命名冲突会直接报错,一个功能的内聚在一个文件里。

同时,即使逻辑只在一个组件中使用,抽成 Composable 也能让 <script setup> 变成清晰的“目录”,提升可读性。


三、编写规范

1. 始终返回包含 ref 的普通对象

不要返回 reactive 对象。 因为使用方解构 reactive 对象会立刻丢失响应性:

// ❌ 错误示范
export function useCounter() {
  const state = reactive({ count: 0 })
  return state
}
// 使用方:const { count } = useCounter() —— count 已失去响应性

// ✅ 正确做法
export function useCounter() {
  const count = ref(0)
  return { count }
}
// 使用方:const { count } = useCounter() —— count 仍是 ref,响应性保留

如果使用方希望以 .count 方式访问(不写 .value),可在使用方用 reactive() 包裹返回值:

const counter = reactive(useCounter(10))
// counter.count 自动解包,仍响应式

注意:对 reactive() 结果再次解构仍会丢失响应性,这是 Vue 响应式系统的基本规则。

2. 调用必须同步

Composable 内部若使用生命周期钩子(onMounted、onUnmounted)或 provide/inject,依赖 Vue 内部的“当前活跃组件实例指针”。该指针只在 setup() 或 <script setup> 顶层同步执行期间有效。

// ❌ 错误:await 之后调用,钩子可能静默失效
async function init() {
  await someAsyncWork()
  const { data } = useFetchData('/api')
}

// ✅ 允许:<script setup> 中的顶层 await
<script setup>
const { data } = await useFetchData('/api')
</script>

在普通 setup() 函数中,await 之后调用 Composable 是不安全的。这类 bug 很难发现——代码“看起来运行了”,只是清理逻辑从未执行。

3. 单一职责

一个 Composable 只负责一个逻辑关注点。数据请求、表单校验、权限判断应拆成独立的函数:

const { user, loading } = useUserProfile()
const { form, errors, validate } = useProfileForm(user)
const { canEdit } = usePermissions(user)

4. 命名以 use 开头

便于识别,也符合社区约定(useFetch、useMouse、useLocalStorage)。


四、复杂场景示例

// useFetchData.js
import { ref, onMounted } from 'vue'

export function useFetchData(apiUrl) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  const fetchData = async () => {
    loading.value = true
    try {
      const res = await fetch(apiUrl)
      data.value = await res.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, error, loading, fetchData }
}

组件中:

<script setup>
import { useFetchData } from './useFetchData'
const { data, error, loading } = useFetchData('https://api.example.com/data')
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <pre v-else>{{ data }}</pre>
</template>

五、与 React Hooks 的本质区别

维度Vue ComposableReact Hooks
执行模型setup 只执行一次,后续由响应式系统驱动每次渲染重新执行整个函数体
调用顺序约束无,可在条件分支中调用(同步路径内)有,不能放在条件/循环中
依赖追踪computed/watchEffect 自动追踪手动写依赖数组,漏写导致闭包旧值
状态匹配方式无隐式匹配依靠调用顺序匹配内部状态

但 Vue 并非没有闭包问题——在异步回调中读取非响应式普通变量同样会拿到旧值,只是形态不同。


六、可测试性

Vue 官方将 Composable 分为两类,测试方式不同:

不依赖组件实例的(只用 ref、computed、watch):直接调用、断言返回值即可。

依赖组件实例的(用了 onMounted、provide/inject):需要宿主组件包装。官方提供的 helper:

export function withSetup(composable) {
  let result
  const app = createApp({
    setup() {
      result = composable()
      return () => {}
    }
  })
  app.mount(document.createElement('div'))
  return [result, app]
}

设计时可以考虑:把请求触发改为由使用方显式调用(返回 fetchData),而非在内部 onMounted 自动执行,这样不依赖组件实例,测试更简单。


七、三条核心要点

  1. 返回包含 ref 的普通对象,不要返回 reactive,否则解构即失去响应性。
  2. 调用必须同步,异步操作之后的调用可能让生命周期钩子静默失效。
  3. 测试方式取决于是否依赖组件实例,设计时优先考虑可独立测试的形态。

做到这三点,加上单一职责和 use 命名约定,就能在项目中稳定、可维护地使用 Composable 函数。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值