从选项式 API(Options API)迁移到组合式 API(Composition API)并不是推倒重来,而是在同一个 Vue 3 项目里,用另一种更灵活的方式重新组织已有的代码。核心目标不是“语法更新”,而是让逻辑更内聚、复用更方便。
第一步:理解两种写法的对应关系,而不是死记硬背
先建立一张基础映射表,把选项式中的核心概念“翻译”成组合式的写法:
| 选项式 API | 组合式 API(<script setup>) |
| ------------------- | --------------------------------------------------- |
| data | ref() 或 reactive() |
| computed | computed() |
| watch | watch() 或 watchEffect() |
| methods | 普通函数 |
| mounted | onMounted() |
| props | defineProps() |
| emits | defineEmits() |
| this.xxx | 直接使用变量名,无需 this |
不需要一次性全背下来。先看一个简单组件的改造,体会“去掉 this、逻辑集中”的感觉。
选项式写法:
export default {
data() {
return {
count: 0,
double: 0
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('组件挂载,当前计数:', this.count)
}
}
组合式写法(<script setup>):
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('组件挂载,当前计数:', count.value)
})
一眼看去,组合式版本将所有与“计数”相关的状态、计算、方法、生命周期写在一起,不再分散在 data、computed、methods 等不同选项中。这就是组合式 API 最大的心智模型变化:按功能组织,而不是按选项类型组织。
第二步:从单个组件开始,渐进改造
你不需要把整个项目一次性全改成组合式 API。Vue 3 完美兼容选项式写法,两者可以在同一个项目中并存。建议采用以下改造节奏:
- 先改新组件
所有新增组件直接使用 <script setup> + 组合式 API。这是成本最低的一步,团队成员可以马上开始习惯新写法。
- 再改“痛点组件”
项目中总有一些逻辑特别复杂、或者因 Mixin 导致命名冲突和来源不清的组件。这些组件改为组合式 API 后,可以用自定义 Hook 把可复用的逻辑抽出来,可读性和可维护性会立刻提升。典型的改造目标:大量使用 Mixin 的组件、需要通过 $refs 操作子组件逻辑的组件。
- 最后统一存量代码
如果项目维护周期还很长,可以逐步把剩余的老组件也改为组合式 API。可以借助官方的迁移工具(如 vue-codemod)批量处理,但人工复查仍然必不可少。
第三步:把 Mixin 改造成自定义 Hook
选项式 API 时代,逻辑复用主要靠 Mixin。但 Mixin 有几个知名痛点:命名冲突、来源不透明、隐式依赖。组合式 API 的自定义 Hook(也叫组合式函数)彻底解决了这些问题。
改造的核心做法:把 Mixin 中的 data、computed、methods、生命周期勾子,都搬到一个独立的 useXxx 函数里。
// 旧写法:一个处理表单验证的 Mixin
const formMixin = {
data() {
return { formValid: false }
},
methods: {
validateForm() { /* ... */ }
}
}
// 新写法:一个自定义 Hook
import { ref } from 'vue'
export function useFormValidation() {
const formValid = ref(false)
function validateForm() { /* ... */ }
return { formValid, validateForm }
}
然后在组件中调用:
const { formValid, validateForm } = useFormValidation()
所有变量和方法的来源一目了然,也不再有命名冲突的风险。
第四步:处理 this 的消失
组合式 API 中没有 this,这会让一些依赖 this 的老代码需要调整。
- 访问路由和状态管理
以前用 this.$router、this.$route、this.$store,现在需要使用组合式 API 对应的函数:
// Vue Router
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Pinia
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
- 获取 DOM 元素
以前用 this.$refs.xxx,现在改用模板 ref:
const myDiv = ref(null)
// 模板中 <div ref="myDiv"></div> 改为 <div ref="myDiv"></div>
// 访问时用 myDiv.value
- 访问全局属性
如果之前通过 app.config.globalProperties 挂载了全局方法或变量,在组合式 API 中可以使用 getCurrentInstance() 或更推荐的方式:把它们封装成独立的 Hook 或通过 provide/inject 传递。
第五步:利用 <script setup> 语法糖减少样板代码
<script setup> 是组合式 API 的推荐书写方式,它在编译时做处理,省去 setup() 函数的返回值和 export default。顶层的变量和函数自动暴露给模板,写法最为简洁。
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() { count.value++ }
</script>
如果组件有 Props 和 Emits,使用编译器宏声明:
const props = defineProps({ title: String })
const emit = defineEmits(['update'])
改造中的常见坑点
.value遗忘:在<script setup>中访问ref的值需要.value,模板中不需要。写习惯后就好了。- 响应式丢失:如果从
reactive对象中解构出基本类型的属性,会丢失响应式。始终用toRefs()解构,或者直接使用ref。 - 生命周期钩子名称:选项式的
created和beforeCreate在组合式中没有直接对应,因为setup()就执行在这两个阶段之间,直接在setup中写初始化逻辑即可。
一个关键心态:好坏标准不是“用组合式 API”,而是“代码是否更好维护”
选项式 API 本身并没有被淘汰,Vue 官方也明确表示它会继续存在。改造的目的不是追求时髦,而是当你的组件逻辑变得复杂、混入难以追踪时,组合式 API 能给你一个更清晰的解。如果一个小组件用选项式已经非常清晰,甚至可以不动它。
一句话总结改造路径:从新组件开始用 <script setup>,把 Mixin 拆成 Hook,遇到 this 换成对应的组合式函数,保持渐进,按需迁移。