Vue 2 到 Vue 3 的语法演进:从 Options API 到 Composition API 完全指南
前言
在前几篇博客中,我们从 Vue 项目的脚手架选型聊到 VSCode 配置,再到 Vue 的核心指令。今天,我们终于要触及 Vue 3 最核心、最颠覆性的变化——Composition API(组合式 API)。
如果你是一位 Vue 2 的老用户,初次接触 Composition API 时可能会有一种“这不像 Vue”的陌生感。如果你是一位刚入门的新手,面对 Options API 和 Composition API 两种写法,可能会陷入“我到底该学哪个”的困惑。
本文将带你系统理解从 Options API 到 Composition API 的演进逻辑,通过大量代码对比和真实项目案例,帮你建立起清晰的认知——不仅知道怎么写,更知道为什么这么写。
一、为什么要从 Options API 走向 Composition API?
1.1 Options API 是什么?
Options API 是 Vue 2 及 Vue 3 中依然支持的传统写法。它将组件的逻辑分散到不同的“选项”中——data、methods、computed、watch、mounted等。
// Options API 写法 —— Vue 2 和 Vue 3 都支持exportdefault{data(){return{count:0,user:{name:'',age:0}}},computed:{doubledCount(){returnthis.count*2}},methods:{increment(){this.count++}},mounted(){console.log('组件已挂载')}}这种写法对初学者非常友好,结构清晰,一看就懂。在小型项目中,Options API 依然是一个不错的选择。
1.2 Options API 的痛点
然而,当项目变得复杂时,Options API 的局限性就会暴露出来。
痛点一:逻辑分散,难以维护
假设我们要实现一个用户信息表单,包含:加载用户数据、表单验证、提交保存、加载状态管理等。在 Options API 中,这些逻辑会被强制分散到不同的选项中:
exportdefault{data(){return{// 用户数据相关user:{name:'',email:''},// 加载状态相关loading:false,// 验证错误相关errors:{}}},methods:{// 加载用户数据的方法fetchUser(){/* ... */},// 表单验证的方法validateForm(){/* ... */},// 提交保存的方法submitForm(){/* ... */}},computed:{// 与用户数据相关的计算isFormValid(){/* ... */}},mounted(){// 加载用户数据this.fetchUser()}}同一个功能(比如“加载用户数据”)的代码散落在data、methods、mounted三个不同的地方。当组件有十几个功能时,你需要在不同选项之间来回跳转,代码的可读性和可维护性急剧下降。
痛点二:逻辑复用困难
在 Options API 中,逻辑复用主要靠Mixins(混入)。但 Mixins 存在几个致命问题:
- 命名冲突:两个 Mixin 定义了同名方法,后者会覆盖前者
- 来源不清晰:组件中使用的属性和方法不知道来自哪个 Mixin
- 隐式依赖:Mixin 之间可能相互依赖,难以追踪
1.3 Composition API 的解决方案
Composition API 的核心思想是:按功能组织代码,而不是按类型分类。
如果说 Options API 是把所有“写周报”需要的东西放一起、所有“画设计稿”需要的东西放一起,那么 Composition API 就是:做什么事,拿什么包,干净利落。
Composition API 通过setup()函数将相关的状态、方法、计算属性、生命周期钩子放在一起,使得代码组织更加灵活和清晰。
二、核心语法对比:从 Options API 到 Composition API
2.1 响应式数据定义
Options API(Vue 2 & Vue 3):
exportdefault{data(){return{count:0,user:{name:'张三',age:25}}}}Composition API(Vue 3):
import{ref,reactive}from'vue'exportdefault{setup(){// 基本类型用 refconstcount=ref(0)// count 是一个 Ref 对象,通过 .value 访问// 对象类型用 reactiveconstuser=reactive({name:'张三',age:25})return{count,user}}}refvsreactive的使用建议:
| 场景 | 推荐 API | 原因 |
|---|---|---|
| 基本类型(number、string、boolean) | ref | reactive不支持基本类型 |
| 对象/数组(需要整体替换) | ref | reactive重新赋值会丢失响应式 |
| 复杂对象(表单、配置项) | reactive | 无需.value,访问更自然 |
| 不确定用哪个 | ref | 更通用,适合任意类型 |
2.2 计算属性
Options API:
exportdefault{data(){return{message:'Hello, Vue!'}},computed:{reversedMessage(){returnthis.message.split('').reverse().join('')}}}Composition API:
import{ref,computed}from'vue'exportdefault{setup(){constmessage=ref('Hello, Vue!')constreversedMessage=computed(()=>{returnmessage.value.split('').reverse().join('')})return{message,reversedMessage}}}2.3 方法
Options API:
exportdefault{data(){return{count:0}},methods:{increment(){this.count++}}}Composition API:
import{ref}from'vue'exportdefault{setup(){constcount=ref(0)constincrement=()=>{count.value++}return{count,increment}}}注意:在 Composition API 中,方法就是普通的 JavaScript 函数,不再需要通过
methods选项包裹。
2.4 生命周期钩子
Vue 3 将生命周期钩子转换为可导入的函数形式:
| Vue 2 Options API | Vue 3 Composition API |
|---|---|
beforeCreate | 不需要(setup()之前执行) |
created | 不需要(setup()之前执行) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
Options API:
exportdefault{data(){return{count:0}},mounted(){console.log('组件已挂载,count =',this.count)},beforeDestroy(){console.log('组件即将销毁')}}Composition API:
import{ref,onMounted,onBeforeUnmount}from'vue'exportdefault{setup(){constcount=ref(0)onMounted(()=>{console.log('组件已挂载,count =',count.value)})onBeforeUnmount(()=>{console.log('组件即将销毁')})return{count}}}2.5 侦听器(watch)
Options API:
exportdefault{data(){return{searchText:''}},watch:{searchText(newVal,oldVal){console.log('搜索词变化:',oldVal,'->',newVal)}}}Composition API:
import{ref,watch}from'vue'exportdefault{setup(){constsearchText=ref('')watch(searchText,(newVal,oldVal)=>{console.log('搜索词变化:',oldVal,'->',newVal)})return{searchText}}}三、<script setup>语法糖:更简洁的 Composition API
在 Vue 3.2 中,官方引入了<script setup>语法糖,让 Composition API 的写法更加简洁。
使用setup()函数的写法:
<script> import { ref, onMounted } from 'vue' export default { setup() { const count = ref(0) const increment = () => count.value++ onMounted(() => { console.log('mounted') }) return { count, increment } // 必须手动 return } } </script>使用<script setup>语法糖的写法:
<script setup> import { ref, onMounted } from 'vue' // 直接定义,无需 return const count = ref(0) const increment = () => count.value++ onMounted(() => { console.log('mounted') }) </script><script setup>的优势:
- 代码更少:不需要
export default和return - 更自然的书写体验:顶层的变量和函数自动暴露给模板
- 更好的性能:编译时优化,避免了不必要的代理层和
this上下文处理 - 更好的 TypeScript 支持:类型推断更加精准
官方推荐:如果你使用单文件组件(SFC)和 Composition API,
<script setup>是官方推荐的语法。
四、真实项目实战:从 Options API 到 Composition API
4.1 场景一:用户信息表单(中型组件)
假设我们要实现一个用户信息编辑表单,包含:加载用户数据、表单验证、提交保存、加载状态管理。
Options API 实现(代码分散) :
<script> export default { data() { return { // ---- 用户数据 ---- user: { name: '', email: '', phone: '' }, // ---- 表单状态 ---- loading: false, errors: {}, isSubmitting: false } }, computed: { // ---- 表单验证 ---- isFormValid() { return this.user.name.trim() && this.user.email.trim() && this.user.phone.trim() } }, methods: { // ---- 加载数据 ---- async fetchUser() { this.loading = true try { const res = await api.getUser(this.$route.params.id) this.user = res.data } finally { this.loading = false } }, // ---- 表单验证 ---- validateField(field) { if (!this.user[field]?.trim()) { this.errors[field] = '此字段为必填项' } else { delete this.errors[field] } }, // ---- 提交保存 ---- async submitForm() { this.isSubmitting = true try { await api.updateUser(this.user) this.$message.success('保存成功') } finally { this.isSubmitting = false } } }, mounted() { this.fetchUser() } } </script>Composition API 实现(按功能分组) :
<script setup> import { ref, reactive, computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { ElMessage } from 'element-plus' import api from '@/api' const route = useRoute() // ====== 1. 用户数据管理 ====== const user = reactive({ name: '', email: '', phone: '' }) const loading = ref(false) const fetchUser = async () => { loading.value = true try { const res = await api.getUser(route.params.id) Object.assign(user, res.data) // 保持响应式 } finally { loading.value = false } } // ====== 2. 表单验证 ====== const errors = reactive({}) const isFormValid = computed(() => { return user.name.trim() && user.email.trim() && user.phone.trim() }) const validateField = (field) => { if (!user[field]?.trim()) { errors[field] = '此字段为必填项' } else { delete errors[field] } } // ====== 3. 提交保存 ====== const isSubmitting = ref(false) const submitForm = async () => { isSubmitting.value = true try { await api.updateUser(user) ElMessage.success('保存成功') } finally { isSubmitting.value = false } } // ====== 4. 初始化加载 ====== onMounted(() => { fetchUser() }) </script>对比总结:Composition API 版本将代码按功能模块(用户数据管理、表单验证、提交保存)清晰分组,每个功能的相关代码集中在一起,一目了然。
4.2 场景二:逻辑复用 —— 封装组合式函数
这是 Composition API 最强大的能力之一——将可复用的逻辑封装成组合式函数(Composables)。
场景:多个页面都需要实现列表数据的分页加载功能。
封装usePagination组合式函数:
// composables/usePagination.jsimport{ref,reactive,computed}from'vue'exportfunctionusePagination(fetchFn){// 状态constlist=ref([])constloading=ref(false)constpagination=reactive({page:1,pageSize:20,total:0})// 计算属性consthasMore=computed(()=>{returnpagination.total>pagination.page*pagination.pageSize})// 方法constloadData=async(reset=false)=>{if(reset){pagination.page=1list.value=[]}loading.value=truetry{constres=awaitfetchFn({page:pagination.page,pageSize:pagination.pageSize})list.value=reset?res.items:[...list.value,...res.items]pagination.total=res.total}finally{loading.value=false}}constloadMore=()=>{if(hasMore.value&&!loading.value){pagination.page++loadData()}}// 返回给组件使用的 APIreturn{list,loading,pagination,hasMore,loadData,loadMore}}在组件中使用:
<script setup> import { usePagination } from '@/composables/usePagination' import { getOrderList } from '@/api/order' // 只需一行代码,即可获得完整的分页能力 const { list, loading, loadData, loadMore } = usePagination(getOrderList) // 组件挂载时自动加载第一页 loadData(true) </script> <template> <div> <div v-for="item in list" :key="item.id">{{ item.name }}</div> <button @click="loadMore" :disabled="loading">加载更多</button> </div> </template>组合式函数的命名规范:统一使用use前缀,如usePagination、useTable、useForm等。
对比 Mixins:组合式函数彻底解决了 Mixins 的命名冲突和来源不清晰的问题。每个组合式函数返回的对象都是显式的,你可以清楚地知道每个属性和方法来自哪里。
五、Vue 2 到 Vue 3 的其他语法变化
5.1 响应式系统:从defineProperty到Proxy
这是 Vue 3 最底层的变革:
| 对比维度 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 新增属性 | 需要Vue.set() | 直接添加即可触发更新 |
| 删除属性 | 需要Vue.delete() | 直接delete即可触发更新 |
| 数组索引修改 | 无法检测 | ✅ 支持 |
| 性能 | 较慢 | 提升约 2 倍 |
5.2 全局 API 的变化
| Vue 2 | Vue 3 |
|---|---|
new Vue() | createApp() |
Vue.component() | app.component() |
Vue.directive() | app.directive() |
Vue.mixin() | app.mixin() |
Vue.use() | app.use() |
5.3v-model的变化
| 对比 | Vue 2 | Vue 3 |
|---|---|---|
| 默认 prop | value | modelValue |
| 默认事件 | input | update:modelValue |
| 多个绑定 | 不支持 | ✅ 支持v-model:name、v-model:email |
.sync修饰符 | ✅ 支持 | ❌ 已移除(用v-model:propName替代) |
5.4 移除的 API
$on、$off、$once(实例事件方法被移除)filters过滤器(推荐用计算属性或方法替代)$children(推荐使用$refs或组件间通信).native修饰符(Vue 3 自动将事件监听添加到根元素)
六、如何选择:Options API vs Composition API?
6.1 选择 Options API 的场景
- 小型项目或简单组件:逻辑简单,Options API 足够
- 团队对 Vue 2 非常熟悉:迁移成本考虑
- 展示型组件:主要是展示数据,逻辑很轻
6.2 选择 Composition API 的场景
- 中大型项目:代码组织更清晰,可维护性更强
- 复杂组件:多个功能交织,需要按逻辑分组
- 需要逻辑复用:多个组件共享相同逻辑
- 使用 TypeScript:Composition API 的类型推断更精准
- 新项目:拥抱未来,Vue 3 的官方推荐方向
6.3 迁移策略
对于已有的 Vue 2 项目,不建议一次性全部重写:
- 新组件使用 Composition API:在新功能或新页面中率先使用
- 逐步迁移旧组件:在有重构需求时,将旧组件改为 Composition API
- 使用
@vue/compat迁移构建:在 Vue 3 上运行 Vue 2 代码,观察兼容警告并逐项修复
七、总结
从 Options API 到 Composition API 的演进,本质上是 Vue 从框架主导走向开发者主导的转变:
| 维度 | Options API(Vue 2) | Composition API(Vue 3) |
|---|---|---|
| 代码组织 | 按类型分类(data、methods、computed) | 按功能分组(相关逻辑放在一起) |
| 逻辑复用 | Mixins(命名冲突、来源不清晰) | 组合式函数(显式、可追踪) |
| TypeScript 支持 | 基础支持,this类型推断困难 | 原生支持,类型推断精准 |
| 学习曲线 | 平缓,对初学者友好 | 陡峭,需要理解新的编程范式 |
| 适用场景 | 小型项目、简单组件 | 中大型项目、复杂组件 |
Composition API 不是要取代 Options API,而是为开发者提供了一个更强大、更灵活的选择。Vue 3 同时支持两种 API 风格,你可以在同一个项目中混合使用。
理解这段演进历程,不仅能帮你写出更好的 Vue 代码,更能让你在面对新技术时,保持一份“知其然,知其所以然”的从容。
