Vue 3 组合式 API 深度实践

团队新项目决定用 Vue 3 + 组合式 API,从选项式迁移过来,踩了不少坑。

为什么选组合式 API

选项式 API 写久了,发现几个问题:

  1. 逻辑分散:同一个功能的数据、方法、生命周期分布在不同选项里
  2. 复用困难:mixin 有命名冲突问题,高阶组件写着麻烦
  3. TypeScript 支持不够好

组合式 API 理论上能解决这些问题,实际用下来呢?

对比

同样的功能,两种写法:

<!-- 选项式 -->
<script>
export default {
  data() {
    return {
      count: 0,
      user: null,
    };
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    },
  },
  methods: {
    increment() {
      this.count++;
    },
  },
  async created() {
    this.user = await fetchUser();
  },
};
</script>

<!-- 组合式 -->
<script setup>
import { ref, computed, onMounted } from 'vue';

const count = ref(0);
const doubleCount = computed(() => count.value * 2);
const user = ref(null);

function increment() {
  count.value++;
}

onMounted(async () => {
  user.value = await fetchUser();
});
</script>

组合式写法更”原生”,但需要习惯 .value

逻辑复用

这是组合式 API 的杀手锏。把相关逻辑抽成可复用的函数:

// useCounter.js
import { ref, computed } from 'vue';

export function useCounter(initial = 0) {
  const count = ref(initial);
  const double = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return { count, double, increment, decrement };
}

使用:

<script setup>
import { useCounter } from './useCounter';

const { count, double, increment } = useCounter(10);
</script>

比 mixin 清晰多了,没有命名冲突,来源一目了然。

代码组织对比

踩过的坑

1. 解构失去响应性

// 错误
const { count, name } = props;  // 失去响应性

// 正确
const { count, name } = toRefs(props);
// 或者
const count = toRef(props, 'count');

2. ref vs reactive

const state = reactive({
  count: 0,
  name: 'test',
});

// 如果要整体替换
state = { count: 1, name: 'new' };  // 报错!

// 用 ref 就行
const state = ref({ count: 0, name: 'test' });
state.value = { count: 1, name: 'new' };  // OK

我的经验:对象用 reactive,需要整体替换的用 ref。简单值用 ref。

3. watch 的坑

// 监听 ref
watch(count, (newVal) => {});  // OK

// 监听 reactive 对象的属性
watch(state.count, () => {});  // 不生效!

// 正确写法
watch(() => state.count, () => {});  // 用 getter

4. provide/inject 的类型

// 提供者
const theme = ref('light');
provide('theme', theme);

// 注入者
const theme = inject<Ref<string>>('theme');  // 类型推断正确

但如果没有提供值,会是 undefined,需要处理:

const theme = inject<Ref<string>>('theme', ref('light'));  // 默认值

组合式函数设计原则

原则说明
命名以 use 开头,如 useCounter
参数接受 ref 或 reactive,增加灵活性
返回值返回 ref 和方法,保持响应性
副作用清理在 onUnmounted 中清理

示例:

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function handleMove(e) {
    x.value = e.clientX;
    y.value = e.clientY;
  }

  onMounted(() => {
    window.addEventListener('mousemove', handleMove);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', handleMove);
  });

  return { x, y };
}

Vue DevTools 调试

迁移策略

我们项目分三步迁移:

  1. 新功能用组合式 API
  2. 独立的 mixin 改成组合式函数
  3. 逐步重构选项式组件

没搞”一刀切”,进度可控。

TypeScript 支持

组合式 API 对 TypeScript 支持好很多:

interface User {
  id: number;
  name: string;
}

const user = ref<User | null>(null);
const users = ref<User[]>([]);

// 类型推断正确
user.value?.name;
users.value[0].id;

选项式 API 用 TypeScript 需要额外配置,体验一般。

总结

组合式 API 的学习曲线比预想的要陡,尤其是响应式系统。但习惯了之后,代码组织确实更清晰。

推荐顺序:

  1. 先在新功能上试
  2. 写几个组合式函数体会复用的好处
  3. 理解 ref/reactive 的区别
  4. 再决定要不要全面迁移

不要为了”先进”而强行迁移,看团队情况和项目需求。