Vue 3 组合式 API 深度实践
团队新项目决定用 Vue 3 + 组合式 API,从选项式迁移过来,踩了不少坑。
为什么选组合式 API
选项式 API 写久了,发现几个问题:
- 逻辑分散:同一个功能的数据、方法、生命周期分布在不同选项里
- 复用困难:mixin 有命名冲突问题,高阶组件写着麻烦
- 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 };
}
迁移策略
我们项目分三步迁移:
- 新功能用组合式 API
- 独立的 mixin 改成组合式函数
- 逐步重构选项式组件
没搞”一刀切”,进度可控。
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 的学习曲线比预想的要陡,尤其是响应式系统。但习惯了之后,代码组织确实更清晰。
推荐顺序:
- 先在新功能上试
- 写几个组合式函数体会复用的好处
- 理解 ref/reactive 的区别
- 再决定要不要全面迁移
不要为了”先进”而强行迁移,看团队情况和项目需求。