组合式 API 与逻辑复用设计模式
组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础。
一、核心设计理念
组合式 API(Composition API)是 Vue 3 引入的一组全新 API,旨在解决选项式 API(Options API)在大型组件中逻辑分散、复用困难的问题。其核心设计理念包括:
1. 函数式编程思维
将组件的逻辑视为函数的组合,而不是选项对象的堆积。响应式状态、计算属性、侦听器、生命周期钩子都可以在 setup 函数内部通过显式调用(如 ref、computed、watch、onMounted)来声明。这种风格更接近原生 JavaScript 的函数式编程,使得逻辑更加灵活、可测试。
2. 按功能聚合逻辑(而非按选项类型拆分)
在选项式 API 中,一个功能的代码(如数据、方法、生命周期、侦听器)被迫分散在 data、methods、mounted、watch 等不同选项中,导致“关注点分散”。组合式 API 允许将同一个逻辑关注点(如“用户信息管理”、“表单验证”)的所有相关代码放在一起,通过自定义组合函数(Composables)进行封装和复用。
// 选项式:同一功能散落各处
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。
<script setup>
import { ref } from "vue";
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
编译后近似为:
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 | 通过 defineProps、defineEmits 宏定义,编译时处理 | 通过 setup(props, context) 参数定义 |
| 类型推导 | 对 TypeScript 支持更好,宏自动推导类型 | 需要手动标注类型 |
顶层 await | 支持,编译为 setup() 中的异步立即执行函数 | 需要包装为 IIFE 或手动处理 |
优势总结:更少的样板代码、更自然的组件定义、更好的 TypeScript 集成、支持顶层 await(配合 Suspense)。
三、自定义组合函数(Composables)设计模式
Composable 是一个利用组合式 API 封装有状态逻辑的函数,遵循特定的设计约定。
1. 命名规范与返回值设计
- 命名:以
use开头,如useUser、useFetch、useCounter。 - 返回值:通常返回一个包含响应式状态和操作函数的对象。如果只需要返回单个 ref,也可以直接返回该 ref,但推荐使用对象方便扩展。
// 好的返回值设计
function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
return { count, increment, decrement };
}
2. 响应式状态的生命周期管理
Composable 内部可以使用生命周期钩子(onMounted、onUnmounted 等),这些钩子会绑定到调用该 composable 的组件实例上。
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工具函数)来统一处理。 - 返回值保持响应式:如果内部状态是基于外部参数计算得出的,应使用
computed或ref包装。
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() 帮助函数,简化这种模式:
import { toValue } from "vue";
function useDouble(input) {
const double = computed(() => toValue(input) * 2);
return { double };
}
4. 异步组合函数模式:处理加载状态、错误、取消请求的标准化结构
典型的异步 composable 返回一个对象,包含 data、error、loading 以及一个 execute 函数(用于手动触发或重试)。
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 };
}
使用示例:
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 接收参数显式传入所需数据,依赖关系透明。
// Mixins 的问题
mixins: [userMixin, loggerMixin];
this.userName; // 来自哪个 mixin?被覆盖了没?
// Composables 的解决方案
const { name: userName } = useUser();
const { log } = useLogger();
五、跨组件状态共享的 Composables
在 Vue 中,如果需要多个组件共享同一份响应式状态,可以将响应式状态定义在模块作用域(而非 composable 函数内部),然后 composable 函数引用并暴露该状态。
// 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. 基本类型推导
ref、reactive、computed 等 API 可以自动推导类型,也支持泛型显式指定。
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 可以定义泛型参数,以支持灵活的类型输入输出。
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 作为参数时的类型
使用 MaybeRef 或 MaybeRefOrGetter 工具类型(Vue 3.3+ 提供的类型实用程序)。
import type { MaybeRef } from "vue";
function useDouble(input: MaybeRef<number>) {
const double = computed(() => unref(input) * 2);
return { double };
}
4. 返回值类型简化
可以使用 ReturnType 或显式定义返回类型,提高可读性。
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 | 组合式函数 |
相关文章
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
状态管理:Pinia 与 Vuex
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 新项目默认选型)。Vuex 4 仍可与 Vue 3 配合,用于维护旧项目。见 Vue 基础。
Vue 生态关键库深度集成
VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。
依赖收集与派发更新机制
说明 Vue 3 中 track / trigger 与 targetMap 的工作方式。前置:v2-v3 差异。
测试策略
Vue 应用常用 Vitest + @vue/test-utils 做单元/组件测试,Playwright 或 Cypress 做 E2E。见 Vue 基础。
TypeScript 深度集成
Vue 3 源码使用 TypeScript 编写, 可获得良好的类型推导。见 Vue 基础。
Series
vue
4 / 16