我为什么从 Options API 切过来了

用了两年 Options API(datamethodscomputedwatch),切到 Composition API 一开始是抗拒的——好好的东西改什么?直到接手一个 2000 行的组件,datamethods 之间隔了 800 行,上下翻到怀疑人生,才明白逻辑碎片化是多大的坑。

这篇文章不讲文档里有的基础语法,只写我在实际项目里积累的写法和心态转变。

核心认知转变

Options API 思维 Composition API 思维
按”选项类型”组织代码 按”业务逻辑”组织代码
状态分散在 data / computed / watch 相关状态收紧在一个 setup 函数/ composable 里
this.xxx 满天飞 去掉 this,响应式变量直接使用
Mixin 复用逻辑(命名冲突噩梦) Composable 函数复用(干净)

本质:从”分类管理”变成”关注点聚合”。

实战写法

1. 告别 this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Options API —— this 地狱
export default {
data() { return { count: 0, user: null } },
computed: {
doubleCount() { return this.count * 2 }
},
methods: {
increment() { this.count++ },
async fetchUser() { this.user = await api.getUser() }
},
mounted() { this.fetchUser() }
}

// Composition API —— 干净
<script setup lang="ts">
const count = ref(0)
const user = ref(null)
const doubleCount = computed(() => count.value * 2)

function increment() { count.value++ }
async function fetchUser() { user.value = await api.getUser() }

onMounted(() => fetchUser())
</script>

模板里直接用 count,不需要 .value——这是 <script setup> 最大的爽点。

2. Composable:把逻辑拆成乐高

当一个组件超过 300 行,就该抽 composable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// composables/useCounter.ts
export function useCounter(initial = 0) {
const count = ref(initial)
const double = computed(() => count.value * 2)

function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initial }

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

// 组件中使用
<script setup lang="ts">
const { count, double, increment, reset } = useCounter(10)
</script>

一个 composable 包含:状态 + 计算 + 方法 + 生命周期,完整的功能单元。

3. ref vs reactive:只用一个就行

1
2
3
4
5
6
7
8
9
10
// 我几乎只用 ref,原因:
const user = ref({ name: 'webwlx', age: 28 })

// 解构不丢失响应式
const { name, age } = toRefs(user.value)

// reactive 解构就丢了响应式,还得包一层 toRefs
// 而且 reactive 不能替换整个对象
const state = reactive({ count: 0 })
// state = reactive({ count: 1 }) ❌ 不行,引用变了

结论:默认用 ref,除非你非常确定一个对象不需要被整体替换。

4. watchEffect vs watch:场景决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// watchEffect:自动追踪依赖,适合"副作用"
watchEffect(() => {
console.log(`count 变成了 ${count.value}`)
// 自动追踪 count,无需显式声明
})

// watch:精确控制,适合"前后值对比"
watch(count, (newVal, oldVal) => {
if (newVal > 100) showWarning()
})

// 基本规则:
// - 只需要"执行副作用" → watchEffect
// - 需要"知道变了什么" → watch

5. Props 和 Emits 的类型安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// defineProps 带类型
const props = defineProps<{
title: string
count?: number
items: Array<{ id: number; name: string }>
}>()

// 带默认值
const props = withDefaults(defineProps<{
title: string
count?: number
}>(), {
count: 0
})

// defineEmits 带类型
const emit = defineEmits<{
update: [id: number, value: string]
delete: [id: number]
}>()

emit('update', 1, 'hello') // 类型检查通过

6. 模板引用(Template Refs)

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
const inputRef = ref<HTMLInputElement>()

onMounted(() => {
inputRef.value?.focus()
})
</script>

<template>
<input ref="inputRef" />
</template>

7. defineExpose:给父组件暴露方法

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 子组件 -->
<script setup lang="ts">
function validate() { /* ... */ }
function reset() { /* ... */ }

defineExpose({ validate, reset })
</script>

<!-- 父组件 -->
<script setup lang="ts">
const childRef = ref<InstanceType<typeof ChildComponent>>()
childRef.value?.validate()
</script>

迁移策略:别一口气全改

  1. Vue 3 完全兼容 Options API,可以混用,不需要一次性迁移
  2. 新组件全部用 <script setup>,老组件不动
  3. 需要复用的逻辑优先抽成 composable
  4. 特别大的老组件(500 行+)挑一个重构

总结

Composition API 带来的不是”新语法”,是更好的代码组织方式。一旦习惯把所有相关逻辑放在一起,你就再也回不去满屏 this 的日子了。