起因
Vue 2 用 Options API 多年:
<script>
export default {
data() { return { count: 0 }; },
computed: {
doubled() { return this.count * 2; },
},
methods: {
inc() { this.count++; },
},
mounted() { console.log('mounted'); },
};
</script>
清晰简单。但复杂组件痛点:
- 同 feature 的 data / method / watcher 分散在不同 section
- 跨组件复用 logic 难(mixin 有命名冲突)
- TS 类型推导弱
Vue 3 Composition API + <script setup>:
<script setup>
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
function inc() { count.value++; }
onMounted(() => console.log('mounted'));
</script>
更接近 React Hook 思路。重构 6 个月经验:
主要变化
<!-- Options API -->
<template><button @click="inc">{{ count }}</button></template>
<script>
export default {
data() { return { count: 0 }; },
methods: { inc() { this.count++; } },
};
</script>
<!-- Composition API -->
<template><button @click="inc">{{ count }}</button></template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
function inc() { count.value++; }
</script>
data()→ref()/reactive()methods→ 普通 functioncomputed→computed()watch→watch()/watchEffect()- lifecycle →
onMounted/onUnmounted等 - props →
defineProps() - emit →
defineEmits()
ref vs reactive
const count = ref(0); // primitive
count.value++;
console.log(count.value);
const state = reactive({ count: 0, name: '' }); // object
state.count++;
state.name = 'bob';
ref 需 .value 访问(template 中自动 unwrap)。
reactive 用 Proxy 包对象,访问无 .value。
主流偏好:全用 ref(一致性,避免混乱)。
对 object 用 ref({ ... })。
composable (Hook 等价)
跨组件复用 logic:
// composables/useCounter.ts
import { ref } from 'vue';
export function useCounter(initial = 0) {
const count = ref(initial);
const inc = () => count.value++;
const dec = () => count.value--;
return { count, inc, dec };
}
// 任意组件
<script setup>
import { useCounter } from '@/composables/useCounter';
const { count, inc } = useCounter();
</script>
跟 React custom hook 概念一致,命名 useXxx。
替代 Vue 2 mixin(没命名冲突 + 类型推导好)。
TS 类型自动
<script setup lang="ts">
const props = defineProps<{
title: string;
count?: number;
}>();
const emit = defineEmits<{
(e: 'update', value: number): void;
}>();
// props.title typed string
// emit('update', 42) typed
</script>
vs Options API:
<script>
export default {
props: {
title: { type: String, required: true },
count: Number,
},
emits: ['update'],
};
</script>
TS-style 简洁 + 类型自动。
reactivity 注意
const state = ref({ count: 0 });
// ❌ destructure 失 reactive
const { count } = state.value;
count++; // 不更新 view
// ✅ toRefs
const stateRefs = toRefs(state.value);
const { count } = stateRefs;
count.value++;
destructure reactive object 是常见坑。解决用 toRefs。
watch vs watchEffect
// watch(显式 source)
watch(count, (newVal, oldVal) => {
console.log(`count changed: ${oldVal} → ${newVal}`);
});
// watchEffect(auto-track deps)
watchEffect(() => {
console.log(`count: ${count.value}`);
});
watch 像 React useEffect with explicit dep。
watchEffect 自动追踪用到的 ref。
lifecycle
import { onMounted, onUnmounted, onUpdated, nextTick } from 'vue';
onMounted(async () => {
await fetchData();
});
onUnmounted(() => {
cleanup();
});
直观跟 Options API mapping。
provide / inject (Context)
// 父
import { provide, ref } from 'vue';
const theme = ref('dark');
provide('theme', theme);
// 子(任意深度)
import { inject } from 'vue';
const theme = inject('theme');
跟 React Context 类似。typed key:
import { InjectionKey } from 'vue';
const themeKey: InjectionKey<Ref<string>> = Symbol();
provide(themeKey, theme);
const theme = inject(themeKey); // typed Ref<string>
项目迁移经验
Vue 2 → Vue 3 大型项目:
- Vue Compat(Vue 2/3 兼容层)渐进迁移
- component 一个一个改 Composition API(同 file 内可以 Options 跟 Composition 共存暂时)
- mixin → composable
- global API(Vue.use → app.use)
我们 100 component 项目 1 个月迁完。
ROI:
- 代码量降 20-30%(去掉 boilerplate)
- 复杂组件可读性大幅提升
- TS 类型推导显著强
仍能用 Options API
Vue 3 兼容 Options API(permanent)。
没必要强迫迁。新 component 用 Composition,老的稳定就放。
与 React Hook 对比
| Vue Composition | React Hook | |
|---|---|---|
| reactivity | Proxy (auto) | manual setState |
| effect deps | 自动 track | 手动 dep array |
| 模板 | SFC (<template>) |
JSX |
| 性能 | 默认细粒度 | 默认 re-render component |
| 学习 | 中 | 中(dep array 陡) |
Vue reactivity 更"魔法" + 不用记 dep。
React 更显式但 boilerplate 多。
script setup 是关键
只用 Composition API 但没 <script setup> → 仍要 setup() function +
return。冗余。
<script setup>(SFC 顶部)让所有 top-level binding 自动可用 template。
减少 50%+ boilerplate。
强烈推荐每 Vue 3 component 都用。
与 Nuxt 3
Nuxt 3 是 Vue 3 的 meta-framework(Next.js 类比)。
auto-import composable / page-based routing / SSR 等。
新 Vue 项目几乎都 Nuxt 3。
真实 case:复杂 form
老 Vue 2 form 组件(800 行 Options API):
- 30+ field,复杂校验
- 跨 step state
- watcher 一堆
迁 Composition + Pinia + Zod:
- 300 行(-60%)
- 校验集中 schema 内
- step 切换 logic 用 composable 复用
- TS 类型保证
最大改进:思路清晰。一个 file 内"这 feature 的所有相关 code 在一起"
(不是 data / methods / computed / watch 跳跃读)。
踩过的坑
-
forgot .value:
if (count > 5)永远 truthy(ref 对象 truthy)。
count.value > 5。template 内不需要 .value 容易混。 -
reactive deep limit:reactive 是浅 → 嵌套 plain object 不
trigger update。ref(deepObject)或者reactive递归。 -
lifecycle order:onMounted 内调子组件 ref → 子还没 mount。
await nextTick() 或 onMounted nested。 -
props 解构丢 reactivity:
const { title } = defineProps()
title 是 snapshot。const props = defineProps(); props.title保
reactive。Vue 3.5+ 支持解构保留 reactivity。 -
TS 推导慢:复杂 generic component 编译变慢。简化 generic 或
拆分 component。
登录后参与评论。