FORMA

组合式 API 与逻辑复用设计模式

组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础

一、核心设计理念

组合式 API(Composition API)是 Vue 3 引入的一组全新 API,旨在解决选项式 API(Options API)在大型组件中逻辑分散、复用困难的问题。其核心设计理念包括:

1. 函数式编程思维

将组件的逻辑视为函数的组合,而不是选项对象的堆积。响应式状态、计算属性、侦听器、生命周期钩子都可以在 setup 函数内部通过显式调用(如 refcomputedwatchonMounted)来声明。这种风格更接近原生 JavaScript 的函数式编程,使得逻辑更加灵活、可测试。

2. 按功能聚合逻辑(而非按选项类型拆分)

在选项式 API 中,一个功能的代码(如数据、方法、生命周期、侦听器)被迫分散在 datamethodsmountedwatch 等不同选项中,导致“关注点分散”。组合式 API 允许将同一个逻辑关注点(如“用户信息管理”、“表单验证”)的所有相关代码放在一起,通过自定义组合函数(Composables)进行封装和复用。

js
// 选项式:同一功能散落各处
export default {
  data() { return { user: null }; },
  methods: { fetchUser() {...} },
  mounted() { this.fetchUser(); }
}

// 组合式:按功能聚合
import { ref, onMounted } from 'vue';
export default {
  setup() {
    const user = ref(null);
    const fetchUser = async () => { ... };
    onMounted(fetchUser);
    return { user, fetchUser };
  }
}

二、<script setup> 语法糖

1. 编译时转换机制

<script setup> 是 Vue 3.2 引入的编译时语法糖,用于简化组合式 API 的书写。它的底层实现原理是:

  • 编译器将 <script setup> 中的代码重写为普通的 setup() 函数体。
  • 顶层绑定自动暴露:所有在 <script setup> 顶层声明的变量、函数、import 导入的内容,都会被编译器自动添加到 setup() 的返回值中,从而可以直接在模板中使用,无需手动 return
vue
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
  count.value++;
}
</script>
<template>
  <button @click="increment">{{ count }}</button>
</template>

编译后近似为:

js
export default {
  setup() {
    const count = ref(0);
    function increment() {
      count.value++;
    }
    return { count, increment };
  },
};

2. 与普通 setup() 的差异与优势

特性<script setup>普通 setup()
代码量更简洁,无需 return需要手动 return 模板使用的变量/函数
组件注册直接 import 组件即可使用,无需 components 选项需要在 components 中注册
Props / Emits通过 definePropsdefineEmits 宏定义,编译时处理通过 setup(props, context) 参数定义
类型推导对 TypeScript 支持更好,宏自动推导类型需要手动标注类型
顶层 await支持,编译为 setup() 中的异步立即执行函数需要包装为 IIFE 或手动处理

优势总结:更少的样板代码、更自然的组件定义、更好的 TypeScript 集成、支持顶层 await(配合 Suspense)。

三、自定义组合函数(Composables)设计模式

Composable 是一个利用组合式 API 封装有状态逻辑的函数,遵循特定的设计约定。

1. 命名规范与返回值设计

  • 命名:以 use 开头,如 useUseruseFetchuseCounter
  • 返回值:通常返回一个包含响应式状态和操作函数的对象。如果只需要返回单个 ref,也可以直接返回该 ref,但推荐使用对象方便扩展。
js
// 好的返回值设计
function useCounter(initialValue = 0) {
  const count = ref(initialValue);
  const increment = () => count.value++;
  const decrement = () => count.value--;
  return { count, increment, decrement };
}

2. 响应式状态的生命周期管理

Composable 内部可以使用生命周期钩子(onMountedonUnmounted 等),这些钩子会绑定到调用该 composable 的组件实例上。

js
function useMouseTracker() {
  const x = ref(0);
  const y = ref(0);
  function update(e) {
    x.value = e.clientX;
    y.value = e.clientY;
  }
  onMounted(() => window.addEventListener("mousemove", update));
  onUnmounted(() => window.removeEventListener("mousemove", update));
  return { x, y };
}

3. 参数设计:支持 ref 输入、返回值响应式包装

  • 参数可以是 ref 或普通值:使用 toRef / toValue(Vue 3.3+ 的 toValue 工具函数)来统一处理。
  • 返回值保持响应式:如果内部状态是基于外部参数计算得出的,应使用 computedref 包装。
js
function useDouble(input) {
  // input 可能是 number 或 ref<number>
  const double = computed(() => {
    const val = typeof input === "number" ? input : input.value;
    return val * 2;
  });
  return { double };
}

Vue 3.3 提供了 toValue() 帮助函数,简化这种模式:

js
import { toValue } from "vue";
function useDouble(input) {
  const double = computed(() => toValue(input) * 2);
  return { double };
}

4. 异步组合函数模式:处理加载状态、错误、取消请求的标准化结构

典型的异步 composable 返回一个对象,包含 dataerrorloading 以及一个 execute 函数(用于手动触发或重试)。

js
import { ref, isRef, unref } from "vue";

function useFetch(url, options = {}) {
  const data = ref(null);
  const error = ref(null);
  const loading = ref(false);

  const execute = async (overrideUrl) => {
    const targetUrl = overrideUrl ?? unref(url);
    loading.value = true;
    error.value = null;
    try {
      const res = await fetch(targetUrl, options);
      if (!res.ok) throw new Error(res.statusText);
      data.value = await res.json();
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  };

  // 自动请求(可选通过 immediate 控制)
  if (options.immediate !== false) execute();

  // 取消请求(使用 AbortController)
  let abortController;
  const abort = () => abortController?.abort();
  // 改写 execute 支持取消,此处省略完整实现

  return { data, error, loading, execute, abort };
}

使用示例

js
const { data, loading, error, execute } = useFetch("/api/user", { immediate: false });
execute(); // 手动触发

四、从 Mixins 到 Composables 的演进

Mixins 的局限性(Vue 2)

  • 命名冲突:多个 mixin 可能定义同名的 data/methods,发生静默覆盖。
  • 来源不透明:组件中使用的变量来自哪个 mixin 难以追踪。
  • 逻辑分散:一个 mixin 的代码可能分布在多个生命周期钩子中,难以理解。
  • 隐式依赖:mixin 可能依赖于组件中的某些属性或方法,没有显式声明,造成脆弱性。

Composables 如何解决上述问题

  • 显式引用:Composable 的返回值通过解构赋值显式引入,所有使用到的变量都来自明确的函数调用,不会有命名冲突(用户可自行重命名)。
  • 命名控制:由于是普通函数,完全由调用者控制变量名。
  • 逻辑边界清晰:每个 composable 内部的逻辑高度内聚,通过函数调用顺序体现依赖关系。
  • 无隐式依赖:Composable 接收参数显式传入所需数据,依赖关系透明。
js
// Mixins 的问题
mixins: [userMixin, loggerMixin];
this.userName; // 来自哪个 mixin?被覆盖了没?

// Composables 的解决方案
const { name: userName } = useUser();
const { log } = useLogger();

五、跨组件状态共享的 Composables

在 Vue 中,如果需要多个组件共享同一份响应式状态,可以将响应式状态定义在模块作用域(而非 composable 函数内部),然后 composable 函数引用并暴露该状态。

js
// stores/counter.js
import { ref } from "vue";
const count = ref(0);
export function useCounter() {
  const increment = () => count.value++;
  const decrement = () => count.value--;
  return { count, increment, decrement };
}

多个组件调用 useCounter() 将获得同一个 count ref 实例,从而实现跨组件状态共享。这与 Pinia 的实现原理类似(Pinia 在底层也是利用了这种模块单例模式)。

注意:这种方式创建的是全局单例,适用于不需要组件生命周期隔离的场景。若需要每个组件独立的状态,则应将状态定义在 composable 函数内部(即每次调用都创建新的 ref)。

六、组合式 API 的 TypeScript 类型推导

组合式 API 在设计之初就对 TypeScript 提供了一流支持。通过泛型可以创建类型安全的 composable。

1. 基本类型推导

refreactivecomputed 等 API 可以自动推导类型,也支持泛型显式指定。

ts
const count = ref(0); // Ref<number>
const user = ref<User | null>(null); // 显式包含 null,否则 TS 会报「null 不能赋值给 User」

function useNumber(initial: number) {
  const value = ref(initial);
  const double = computed(() => value.value * 2);
  return { value, double }; // 类型自动推导
}

2. 泛型组合函数设计

自定义 composable 可以定义泛型参数,以支持灵活的类型输入输出。

ts
function useFetch<T>(url: string | Ref<string>): {
  data: Ref<T | null>;
  error: Ref<string | null>;
  loading: Ref<boolean>;
} {
  const data = ref<T | null>(null);
  const error = ref<string | null>(null);
  const loading = ref(false);
  // ... 实现
  return { data, error, loading };
}

// 使用时指定类型
const { data } = useFetch<User[]>("/api/users");
// data 的类型为 Ref<User[] | null>

3. 处理 ref 作为参数时的类型

使用 MaybeRefMaybeRefOrGetter 工具类型(Vue 3.3+ 提供的类型实用程序)。

ts
import type { MaybeRef } from "vue";

function useDouble(input: MaybeRef<number>) {
  const double = computed(() => unref(input) * 2);
  return { double };
}

4. 返回值类型简化

可以使用 ReturnType 或显式定义返回类型,提高可读性。

ts
interface UseCounterReturn {
  count: Ref<number>;
  increment: () => void;
}
function useCounter(initial: number = 0): UseCounterReturn {
  const count = ref(initial);
  const increment = () => count.value++;
  return { count, increment };
}

总结

方面关键点
核心设计理念函数式编程、按功能聚合逻辑
<script setup>编译转换、自动暴露、更简洁的语法
Composables 设计useXxx 命名、返回对象、生命周期钩子、支持 ref 参数、异步标准化
Mixins → Composables解决命名冲突、来源不透明、逻辑分散、隐式依赖
跨组件状态共享模块级 ref + composable 返回同一实例
TypeScript 支持泛型组合函数、MaybeRef 类型、自动类型推导

组合式 API 带来的不仅是语法上的改进,更是一种可组合、可预测的逻辑复用模式,是现代 Vue 应用的推荐实践。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:组合式 API官方说明
Vue:<script setup>语法糖
Vue:Composables组合式函数