Vue 3 Composable 函数指南

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


一、是什么

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

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

js 复制代码
// 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 }
}

使用:

vue 复制代码
<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 对象会立刻丢失响应性:

js 复制代码
// ❌ 错误示范
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() 包裹返回值:

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

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

2. 调用必须同步

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

js 复制代码
// ❌ 错误: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 只负责一个逻辑关注点。数据请求、表单校验、权限判断应拆成独立的函数:

js 复制代码
const { user, loading } = useUserProfile()
const { form, errors, validate } = useProfileForm(user)
const { canEdit } = usePermissions(user)

4. 命名以 use 开头

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


四、复杂场景示例

js 复制代码
// 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 }
}

组件中:

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

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


六、可测试性

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

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

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

js 复制代码
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 函数。

相关推荐
tryCbest44 分钟前
Vue3——第 9 篇:组件通信全攻略(下)——Provide / Inject 与事件总线
前端·javascript·vue.js
lichenyang4531 小时前
技术穿刺怎么做?以企业知识库问答为例,验证风险、方案和投入
前端·后端
PedroQue991 小时前
uni-app x工程化实战:从页面堆到可维护架构
前端·uni-app
开开心心_Every1 小时前
免费压缩软件WinRAR无弹窗、广告干扰
java·开发语言·前端·spring·智能手机·word·winrar
特立独行的猫A1 小时前
用仓颉语言写桌面应用:cj-tauri 框架与 AI 笔记软件 cjnote 实战
前端
特立独行的猫A2 小时前
我为什么推荐用仓颉语言的cj-tauri 写桌面应用(附 cjnote 最小的markdown笔记AI软件)
前端
纸片人2 小时前
牛一越界,大屏 3 秒飞到它身边:我用真实卫星数据 + Three.js 做了个 3D 智慧牧场
前端
cc_瀚海知行2 小时前
工具调用 6|助手什么时候该停下来?
java·服务器·前端·人工智能·python·ai