Vue3 的 Composition API vs Vue2 的 Options API:逻辑复用能力的提升

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,也就是将组件逻辑按照 datamethodscomputedwatch 等选项组织在一起。这种方式对简单组件很友好,但一旦组件变复杂,问题就暴露出来了:

1. 逻辑分散到不同选项中

比如一个用户详情页组件,可能包含:

  • 用户信息加载(fetch)
  • 缓存策略(local storage)
  • 表单验证逻辑
  • 响应式状态管理(如 loading / error)

这些逻辑被分散在 datacreatedmethodscomputed 中,阅读和调试变得困难。

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 都有 countincrement 方法呢?你会陷入混乱。


二、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 是一个独立的函数,可以复用于任何表单
  • formerrors 是响应式对象,不会污染全局作用域
  • 逻辑集中在一处,可读性强,且易于单元测试

四、更复杂的场景:权限控制 + 数据缓存

现在我们考虑一个更复杂的例子:一个“用户管理”页面,需要:

  • 获取用户列表(带分页)
  • 缓存最近一次请求结果(避免重复拉取)
  • 权限判断(只有 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>

✅ 优势:

  • useAuthuseCachedApi 是纯函数,可跨组件复用
  • 逻辑职责分明:权限、缓存、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,而是为那些需要更高复用性、更好结构化的项目提供了更强大的工具。它让你写出“像函数一样干净”的组件逻辑,而不是一堆散落的配置项。


七、最佳实践建议(给团队或个人)

  1. 优先使用 <script setup> + Composition API

    • 它是 Vue3 推荐的标准语法糖
    • 更简洁,不需要写 setup() 函数
  2. 命名规范:useXXX 命名你的 Hook

    // ✅ 正确
    function useLocalStorage(key, initialValue) { ... }
    
    // ❌ 不推荐
    function localState(key, init) { ... }
  3. 不要滥用组合:合理拆分

    • 一个 Hook 应该聚焦单一职责(如 useFormValidation
    • 如果超过 50 行,考虑进一步拆分(比如 useEmailValidation, usePasswordValidation
  4. 结合 TypeScript 使用

    interface FormState {
      username: string;
      password: string;
    }
    
    function useFormValidation(): {
      form: FormState;
      errors: Ref<Record<string, string>>;
      validate: () => boolean;
    }
  5. 单元测试友好

    • Hook 可以单独测试,不依赖 DOM
    • 用 Jest/Vitest 测试逻辑本身即可

最后送一句话给大家:

“当你发现自己写了三个相似的 mountedwatch 时,就是时候用 Composition API 来抽象逻辑了。”

这就是 Vue3 带来的真正价值:让开发者不再纠结于“如何组织代码”,而专注于“如何解决问题”。

谢谢大家!欢迎在评论区讨论你的实际项目经验 😊

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注