Vue3 Composition API vs Vue2 Options API:逻辑复用能力的深度解析
大家好,今天我们来深入探讨一个在 Vue 生态中非常关键的话题——逻辑复用能力的提升。这是从 Vue 2 到 Vue 3 过渡过程中最值得重视的变化之一,尤其是当我们面对复杂组件、多业务场景时,这个能力直接决定了代码是否易于维护、扩展和测试。
我们将以讲座的方式展开,逐步拆解:
- Vue2 Options API 的局限性
- Vue3 Composition API 如何解决这些问题
- 实战案例对比(含完整代码)
- 性能与可读性的权衡
- 最佳实践建议
一、Vue2 Options API 的痛点:逻辑分散、难以复用
在 Vue 2 中,我们使用的是 Options API,也就是将组件逻辑按照 data、methods、computed、watch 等选项组织在一起。这种方式对简单组件很友好,但一旦组件变复杂,问题就暴露出来了:
1. 逻辑分散到不同选项中
比如一个用户详情页组件,可能包含:
- 用户信息加载(fetch)
- 缓存策略(local storage)
- 表单验证逻辑
- 响应式状态管理(如 loading / error)
这些逻辑被分散在 data、created、methods、computed 中,阅读和调试变得困难。
2. 复用困难 —— Mixins 的“副作用”
Vue2 提供了 Mixins 来实现逻辑复用,但它存在严重缺陷:
- 名称冲突(多个 mixin 同名属性/方法会被覆盖)
- 难以追踪来源(哪个 mixin 提供了某个方法?)
- 不直观(你不知道一个方法来自哪里)
// Vue2 示例:用 mixins 复用“计数器”逻辑
const CounterMixin = {
data() {
return { count: 0 };
},
methods: {
increment() { this.count++; },
decrement() { this.count--; }
}
};
export default {
mixins: [CounterMixin],
mounted() {
console.log(this.count); // 0
}
};
这看起来没问题,但如果多个 mixin 都有 count 或 increment 方法呢?你会陷入混乱。
二、Vue3 Composition API:让逻辑“聚合”起来
Vue3 引入了 Composition API,它允许你在 <script setup> 中使用函数式方式组织逻辑,核心思想是:按功能组织代码,而不是按选项分类。
核心优势:
| 方面 | Vue2 Options API | Vue3 Composition API |
|---|---|---|
| 逻辑组织 | 按选项划分(data/methods/computed) | 按功能聚合(如 useUser、useForm) |
| 复用方式 | Mixins(易冲突) | 自定义 Hook(函数封装 + 组合) |
| 可读性 | 分散,不易理解 | 聚焦于“做什么”,而非“怎么放” |
| 类型支持 | 有限(需配合 TypeScript) | 原生支持 TS(类型推导更强) |
更重要的是:Composition API 让你真正可以像写普通函数一样写逻辑,然后任意组合它们!
三、实战对比:从“表单校验”开始
假设我们要做一个登录表单,需要:
- 输入框响应式数据(username/password)
- 校验规则(非空、长度)
- 提交处理(调接口)
- 错误提示(显示错误信息)
✅ Vue2 Options API 写法(冗长且难复用)
<!-- LoginForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<span v-if="errors.username">{{ errors.username }}</span>
<span v-if="errors.password">{{ errors.password }}</span>
<button :disabled="loading">提交</button>
</form>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
loading: false,
errors: {}
};
},
methods: {
validate() {
const errs = {};
if (!this.username) errs.username = '用户名不能为空';
if (this.username.length < 3) errs.username = '用户名至少3位';
if (!this.password) errs.password = '密码不能为空';
if (this.password.length < 6) errs.password = '密码至少6位';
this.errors = errs;
return Object.keys(errs).length === 0;
},
async handleSubmit() {
if (!this.validate()) return;
this.loading = true;
try {
await fetch('/api/login', { method: 'POST', body: JSON.stringify({ username: this.username, password: this.password }) });
alert('登录成功');
} catch (e) {
this.errors.general = '网络错误';
} finally {
this.loading = false;
}
}
}
};
</script>
✅ 功能实现了,但问题明显:
- 所有逻辑都在一个文件里,臃肿
- 如果另一个页面也需要类似表单校验?复制粘贴?还是提取成 mixin?
✅ Vue3 Composition API 写法(清晰、可复用)
<!-- LoginForm.vue -->
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.username" placeholder="用户名" />
<input v-model="form.password" type="password" placeholder="密码" />
<span v-if="errors.username">{{ errors.username }}</span>
<span v-if="errors.password">{{ errors.password }}</span>
<button :disabled="loading">提交</button>
</form>
</template>
<script setup>
import { ref, reactive } from 'vue';
// 自定义 Hook:表单校验逻辑
function useFormValidation(initialValues = {}) {
const form = reactive({ ...initialValues });
const errors = ref({});
function validate() {
const errs = {};
if (!form.username) errs.username = '用户名不能为空';
if (form.username.length < 3) errs.username = '用户名至少3位';
if (!form.password) errs.password = '密码不能为空';
if (form.password.length < 6) errs.password = '密码至少6位';
errors.value = errs;
return Object.keys(errs).length === 0;
}
return {
form,
errors,
validate
};
}
// 使用 Hook
const { form, errors, validate } = useFormValidation();
const loading = ref(false);
async function handleSubmit() {
if (!validate()) return;
loading.value = true;
try {
await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(form)
});
alert('登录成功');
} catch (e) {
errors.value.general = '网络错误';
} finally {
loading.value = false;
}
}
</script>
💡 关键点:
useFormValidation是一个独立的函数,可以复用于任何表单form和errors是响应式对象,不会污染全局作用域- 逻辑集中在一处,可读性强,且易于单元测试
四、更复杂的场景:权限控制 + 数据缓存
现在我们考虑一个更复杂的例子:一个“用户管理”页面,需要:
- 获取用户列表(带分页)
- 缓存最近一次请求结果(避免重复拉取)
- 权限判断(只有 admin 才能删除)
Vue2 Options API:难以优雅处理
如果你尝试把所有逻辑放在一个组件里,会变成这样:
export default {
data() {
return {
users: [],
loading: false,
cacheKey: null,
cachedData: null
};
},
computed: {
isAdmin() { return this.$store.getters.user.role === 'admin'; }
},
methods: {
async loadUsers(page = 1) {
const key = `users_${page}`;
if (this.cacheKey === key && this.cachedData) {
this.users = this.cachedData;
return;
}
this.loading = true;
try {
const res = await api.get(`/users?page=${page}`);
this.users = res.data;
this.cacheKey = key;
this.cachedData = res.data;
} finally {
this.loading = false;
}
},
async deleteUser(id) {
if (!this.isAdmin) return;
await api.delete(`/users/${id}`);
this.loadUsers(); // 重新加载
}
}
};
⚠️ 问题:
- 逻辑混杂:权限判断、缓存、API 请求都挤在一个组件里
- 如果其他页面也需要“带缓存的列表”?无法复用
- 测试困难:每个方法都要 mock store 和 api
Vue3 Composition API:模块化设计,轻松复用
<!-- UserList.vue -->
<template>
<div>
<button @click="loadMore">加载更多</button>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
<button v-if="isAdmin" @click="deleteUser(user.id)">删除</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { useCachedApi } from '@/composables/useCachedApi';
// 自定义 Hook:权限控制
function useAuth() {
const user = ref(null);
const isAdmin = computed(() => user.value?.role === 'admin');
return {
user,
isAdmin
};
}
// 自定义 Hook:带缓存的 API 请求
function useCachedApi(apiFn, cacheKeyPrefix = '') {
const cache = new Map();
async function fetch(key, ...args) {
const fullKey = `${cacheKeyPrefix}_${key}`;
if (cache.has(fullKey)) {
return cache.get(fullKey);
}
const result = await apiFn(...args);
cache.set(fullKey, result);
return result;
}
function clearCache() {
cache.clear();
}
return {
fetch,
clearCache
};
}
// 使用多个 Hook
const { isAdmin } = useAuth();
const { fetch } = useCachedApi(async (page) => {
const res = await fetch(`/api/users?page=${page}`);
return res.data;
}, 'users');
const users = ref([]);
const currentPage = ref(1);
async function loadMore() {
const data = await fetch(currentPage.value++);
users.value.push(...data);
}
async function deleteUser(id) {
if (!isAdmin.value) return;
await fetch(`/api/users/${id}`, { method: 'DELETE' });
users.value = users.value.filter(u => u.id !== id);
}
</script>
✅ 优势:
useAuth和useCachedApi是纯函数,可跨组件复用- 逻辑职责分明:权限、缓存、UI 控制各自独立
- 易于测试:你可以单独测试
useAuth是否正确返回isAdmin - 支持 TypeScript 类型推导(无需额外声明)
五、性能与可读性:Composition API 的额外收益
虽然这不是本文重点,但值得一提:
| 维度 | Vue2 Options API | Vue3 Composition API |
|---|---|---|
| 渲染性能 | 相同(依赖收集机制优化) | 更优(更细粒度响应式) |
| 内存占用 | 较高(data 层级深) | 更低(只响应需要的数据) |
| 开发体验 | 差(mixins 混乱) | 好(函数式编程风格) |
| 可测试性 | 低(需 deep mount) | 高(Hook 可独立测试) |
例如,在大型项目中,你可能会发现某些组件因为嵌套太深导致性能瓶颈。Composition API 允许你只暴露必要的响应式变量,减少不必要的监听。
六、总结:为什么说 Composition API 提升了逻辑复用能力?
| 传统方式(Vue2) | 新方式(Vue3) |
|---|---|
| 逻辑分散在 options 中 | 逻辑按功能聚合在函数中 |
| 复用靠 mixins(易冲突) | 复用靠自定义 Hook(纯净无副作用) |
| 难以拆分、重构 | 可轻松拆分为小模块 |
| 适合简单组件 | 更适合复杂、可复用业务逻辑 |
📌 结论:
Composition API 并不是“取代” Options API,而是为那些需要更高复用性、更好结构化的项目提供了更强大的工具。它让你写出“像函数一样干净”的组件逻辑,而不是一堆散落的配置项。
七、最佳实践建议(给团队或个人)
-
优先使用
<script setup>+ Composition API- 它是 Vue3 推荐的标准语法糖
- 更简洁,不需要写
setup()函数
-
命名规范:
useXXX命名你的 Hook// ✅ 正确 function useLocalStorage(key, initialValue) { ... } // ❌ 不推荐 function localState(key, init) { ... } -
不要滥用组合:合理拆分
- 一个 Hook 应该聚焦单一职责(如
useFormValidation) - 如果超过 50 行,考虑进一步拆分(比如
useEmailValidation,usePasswordValidation)
- 一个 Hook 应该聚焦单一职责(如
-
结合 TypeScript 使用
interface FormState { username: string; password: string; } function useFormValidation(): { form: FormState; errors: Ref<Record<string, string>>; validate: () => boolean; } -
单元测试友好
- Hook 可以单独测试,不依赖 DOM
- 用 Jest/Vitest 测试逻辑本身即可
最后送一句话给大家:
“当你发现自己写了三个相似的
mounted或watch时,就是时候用 Composition API 来抽象逻辑了。”
这就是 Vue3 带来的真正价值:让开发者不再纠结于“如何组织代码”,而专注于“如何解决问题”。
谢谢大家!欢迎在评论区讨论你的实际项目经验 😊