Vue 3 Composition API:从 Options API 迁移的实际感受

起因

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 → 普通 function
  • computedcomputed()
  • watchwatch() / 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 大型项目:

  1. Vue Compat(Vue 2/3 兼容层)渐进迁移
  2. component 一个一个改 Composition API(同 file 内可以 Options 跟 Composition 共存暂时)
  3. mixin → composable
  4. 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 跳跃读)。

踩过的坑

  1. forgot .valueif (count > 5) 永远 truthy(ref 对象 truthy)。
    count.value > 5。template 内不需要 .value 容易混。

  2. reactive deep limit:reactive 是浅 → 嵌套 plain object 不
    trigger update。ref(deepObject) 或者 reactive 递归。

  3. lifecycle order:onMounted 内调子组件 ref → 子还没 mount。
    await nextTick() 或 onMounted nested。

  4. props 解构丢 reactivityconst { title } = defineProps()
    title 是 snapshot。const props = defineProps(); props.title
    reactive。Vue 3.5+ 支持解构保留 reactivity。

  5. TS 推导慢:复杂 generic component 编译变慢。简化 generic 或
    拆分 component。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。