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 都支持
export default {
  data() {
    return {
      count: 0,
      user: { name: '', age: 0 }
    }
  },
  computed: {
    doubledCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('组件已挂载')
  }
}

这种写法对初学者非常友好,结构清晰,一看就懂。在小型项目中,Options API 依然是一个不错的选择。

1.2 Options API 的痛点

然而,当项目变得复杂时,Options API 的局限性就会暴露出来。

痛点一:逻辑分散,难以维护

假设我们要实现一个用户信息表单,包含:加载用户数据、表单验证、提交保存、加载状态管理等。在 Options API 中,这些逻辑会被强制分散到不同的选项中:

export default {
  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)

export default {
  data() {
    return {
      count: 0,
      user: { name: '张三', age: 25 }
    }
  }
}

Composition API(Vue 3)

import { ref, reactive } from 'vue'

export default {
  setup() {
    // 基本类型用 ref
    const count = ref(0)  // count 是一个 Ref 对象,通过 .value 访问
    
    // 对象类型用 reactive
    const user = reactive({ name: '张三', age: 25 })
    
    return { count, user }
  }
}

ref vs reactive 的使用建议

场景 推荐 API 原因
基本类型(number、string、boolean) ref reactive 不支持基本类型
对象/数组(需要整体替换) ref reactive 重新赋值会丢失响应式
复杂对象(表单、配置项) reactive 无需 .value,访问更自然
不确定用哪个 ref 更通用,适合任意类型

2.2 计算属性

Options API

export default {
  data() {
    return { message: 'Hello, Vue!' }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  }
}

Composition API

import { ref, computed } from 'vue'

export default {
  setup() {
    const message = ref('Hello, Vue!')
    const reversedMessage = computed(() => {
      return message.value.split('').reverse().join('')
    })
    return { message, reversedMessage }
  }
}

2.3 方法

Options API

export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

Composition API

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const increment = () => {
      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

export default {
  data() {
    return { count: 0 }
  },
  mounted() {
    console.log('组件已挂载,count =', this.count)
  },
  beforeDestroy() {
    console.log('组件即将销毁')
  }
}

Composition API

import { ref, onMounted, onBeforeUnmount } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    onMounted(() => {
      console.log('组件已挂载,count =', count.value)
    })
    
    onBeforeUnmount(() => {
      console.log('组件即将销毁')
    })
    
    return { count }
  }
}

2.5 侦听器(watch)

Options API

export default {
  data() {
    return { searchText: '' }
  },
  watch: {
    searchText(newVal, oldVal) {
      console.log('搜索词变化:', oldVal, '->', newVal)
    }
  }
}

Composition API

import { ref, watch } from 'vue'

export default {
  setup() {
    const searchText = 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.js
import { ref, reactive, computed } from 'vue'

export function usePagination(fetchFn) {
  // 状态
  const list = ref([])
  const loading = ref(false)
  const pagination = reactive({
    page: 1,
    pageSize: 20,
    total: 0
  })
  
  // 计算属性
  const hasMore = computed(() => {
    return pagination.total > pagination.page * pagination.pageSize
  })
  
  // 方法
  const loadData = async (reset = false) => {
    if (reset) {
      pagination.page = 1
      list.value = []
    }
    
    loading.value = true
    try {
      const res = await fetchFn({
        page: pagination.page,
        pageSize: pagination.pageSize
      })
      list.value = reset ? res.items : [...list.value, ...res.items]
      pagination.total = res.total
    } finally {
      loading.value = false
    }
  }
  
  const loadMore = () => {
    if (hasMore.value && !loading.value) {
      pagination.page++
      loadData()
    }
  }
  
  // 返回给组件使用的 API
  return { 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 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.3 v-model 的变化

对比 Vue 2 Vue 3
默认 prop value modelValue
默认事件 input update: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 代码,更能让你在面对新技术时,保持一份“知其然,知其所以然”的从容。

posted @ 2026-07-10 23:29  我有满天星辰  阅读(1)  评论(0)    收藏  举报