Vue 2 到 Vue 3 的语法演进:从 Options API 到 Composition API 完全指南

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 中依然支持的传统写法。它将组件的逻辑分散到不同的“选项”中——datamethodscomputedwatchmounted等。

// 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()}}

同一个功能(比如“加载用户数据”)的代码散落在datamethodsmounted三个不同的地方。当组件有十几个功能时,你需要在不同选项之间来回跳转,代码的可读性和可维护性急剧下降

痛点二:逻辑复用困难

在 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)refreactive不支持基本类型
对象/数组(需要整体替换)refreactive重新赋值会丢失响应式
复杂对象(表单、配置项)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 APIVue 3 Composition API
beforeCreate不需要(setup()之前执行)
created不需要(setup()之前执行)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted

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 defaultreturn
  • 更自然的书写体验:顶层的变量和函数自动暴露给模板
  • 更好的性能:编译时优化,避免了不必要的代理层和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前缀,如usePaginationuseTableuseForm等。

对比 Mixins:组合式函数彻底解决了 Mixins 的命名冲突和来源不清晰的问题。每个组合式函数返回的对象都是显式的,你可以清楚地知道每个属性和方法来自哪里。


五、Vue 2 到 Vue 3 的其他语法变化

5.1 响应式系统:从definePropertyProxy

这是 Vue 3 最底层的变革:

对比维度Vue 2Vue 3
实现方式Object.definePropertyProxy
新增属性需要Vue.set()直接添加即可触发更新
删除属性需要Vue.delete()直接delete即可触发更新
数组索引修改无法检测✅ 支持
性能较慢提升约 2 倍

5.2 全局 API 的变化

Vue 2Vue 3
new Vue()createApp()
Vue.component()app.component()
Vue.directive()app.directive()
Vue.mixin()app.mixin()
Vue.use()app.use()

5.3v-model的变化

对比Vue 2Vue 3
默认 propvaluemodelValue
默认事件inputupdate:modelValue
多个绑定不支持✅ 支持v-model:namev-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 项目,不建议一次性全部重写:

  1. 新组件使用 Composition API:在新功能或新页面中率先使用
  2. 逐步迁移旧组件:在有重构需求时,将旧组件改为 Composition API
  3. 使用@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 代码,更能让你在面对新技术时,保持一份“知其然,知其所以然”的从容。