我为什么从 Options API 切过来了
用了两年 Options API(data、methods、computed、watch),切到 Composition API 一开始是抗拒的——好好的东西改什么?直到接手一个 2000 行的组件,data 和 methods 之间隔了 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
| 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() } }
<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
| 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
| const user = ref({ name: 'webwlx', age: 28 })
const { name, age } = toRefs(user.value)
const state = reactive({ count: 0 })
|
结论:默认用 ref,除非你非常确定一个对象不需要被整体替换。
4. watchEffect vs watch:场景决定
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| watchEffect(() => { console.log(`count 变成了 ${count.value}`) })
watch(count, (newVal, oldVal) => { if (newVal > 100) showWarning() })
|
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
| const props = defineProps<{ title: string count?: number items: Array<{ id: number; name: string }> }>()
const props = withDefaults(defineProps<{ title: string count?: number }>(), { count: 0 })
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>
|
迁移策略:别一口气全改
- Vue 3 完全兼容 Options API,可以混用,不需要一次性迁移
- 新组件全部用
<script setup>,老组件不动
- 需要复用的逻辑优先抽成 composable
- 特别大的老组件(500 行+)挑一个重构
总结
Composition API 带来的不是”新语法”,是更好的代码组织方式。一旦习惯把所有相关逻辑放在一起,你就再也回不去满屏 this 的日子了。