Skip to content

D01 · Options API vs Composition API ​

对应主课: L37 Composition API 设计哲学 最后核对: 2026-09-23


1. 核心区别 ​

维度Options APIComposition API
代码组织按选项类型分组 (data/computed/methods)按功能逻辑分组
逻辑复用mixin、工具函数、组件组合;也可通过 setup 使用 composable用函数显式传入依赖、返回状态与操作
TypeScriptdefineComponent 支持推断,复杂 mixin 合并较难表达普通变量/函数便于推断,仍需为边界和泛型声明类型
this 依赖依赖 this 上下文不依赖 this
组织约束选项提供固定放置位置需要自己划分函数与模块边界
产物差异需要 Options 处理逻辑与实例代理访问script setup 有利于直接访问绑定与压缩;最终体积取决于实际构建

切换语法不会自动让应用代码都可 tree-shake。若关闭 __VUE_OPTIONS_API__ 才能排除相应运行时支持,还必须确认依赖组件也不使用 Options API;普通课程应用保持默认开启。编译标志


2. 同一功能的两种写法 ​

这两个计数器行为相同:增加、限制非负的减少、显示派生值、记录每次变化,重置后历史为空。默认 watcher 会批处理;这里刻意用同步 watcher 记录标量计数的每次修改,reset 先触发记录再清空历史,不会在下一次刷新时重新补出一条记录。

Options API ​

vue
<!-- OptionsCounter.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
interface Change { from: number; to: number; time: number }
export default defineComponent({
  data() {
    return { count: 0, history: [] as Change[] }
  },
  computed: {
    doubleCount(): number { return this.count * 2 },
    isEven(): boolean { return this.count % 2 === 0 },
  },
  watch: {
    count: {
      flush: 'sync',
      handler(newVal: number, oldVal: number) {
        this.history.push({ from: oldVal, to: newVal, time: Date.now() })
      },
    },
  },
  methods: {
    increment() { this.count++ },
    decrement() { if (this.count > 0) this.count-- },
    reset() { this.count = 0; this.history = [] },
  },
  mounted() { console.log('Options 组件已挂载') },
})
</script>

Composition API ​

vue
<!-- CompositionCounter.vue -->
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
interface Change { from: number; to: number; time: number }
const count = ref(0)
const history = ref<Change[]>([])
const doubleCount = computed(() => count.value * 2)
const isEven = computed(() => count.value % 2 === 0)
watch(count, (newVal, oldVal) => {
  history.value.push({ from: oldVal, to: newVal, time: Date.now() })
}, { flush: 'sync' })
function increment() { count.value++ }
function decrement() { if (count.value > 0) count.value-- }
function reset() { count.value = 0; history.value = [] }
onMounted(() => console.log('Composition 组件已挂载'))
</script>

在两份 SFC 的脚本后分别追加同一个模板,然后在主课 Vue 3.5 的 Vite/SFC 项目中导入两个组件,放到同一个实验页面。它们各自持有状态,点击其中一个不会影响另一个:

vue
<template>
  <section>
    <p>计数:{{ count }};两倍:{{ doubleCount }};偶数:{{ isEven }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
    <p>记录数:{{ history.length }}</p>
    <ol><li v-for="(item, index) in history" :key="index">{{ item.from }} → {{ item.to }} / {{ item.time }}</li></ol>
  </section>
</template>

这里的历史只有追加与整组清空,且行内没有局部表单状态;index key 用于这个受限展示。如果扩展为历史排序、删除单条或带子组件状态,应给记录稳定 id。依次点击“增加、增加、减少”,应显示计数 1、两倍 2、三条记录;再点“重置”,计数与记录数都归零。


3. 逻辑复用对比 ​

Mixins 的来源与合并问题 ​

下面是三个文件的结构示意,两个 mixin 都使用同一个 loading 名称:

javascript
// mixin-a.js
export default {
  data() { return { loading: false } },       // ← 和 mixin-b 冲突!
  methods: { start() { this.loading = true } },
}

// mixin-b.js
export default {
  data() { return { loading: false } },       // ← 命名冲突
  methods: { fetchData() { /* ... */ } },
}

// 组件中使用
import mixinA from './mixin-a.js'
import mixinB from './mixin-b.js'
export default {
  mixins: [mixinA, mixinB],
  // 问题 1: loading 来自哪个 mixin?
  // 问题 2: 谁覆盖了谁?
  // 问题 3: 更多 mixin 和隐式依赖会让类型关系更难追踪
}

Composable 显式表达返回值 ​

typescript
// composables/useLoading.ts
import { ref } from 'vue'
export function useLoading() {
  const loading = ref(false)
  function start() { loading.value = true }
  function stop() { loading.value = false }
  return { loading, start, stop }
}
typescript
// 组件的 setup 中使用
import { useLoading } from './composables/useLoading'
const { loading: loadingA, start: startA } = useLoading()
const { loading: loadingB, start: startB } = useLoading()
// ✅ 没有冲突,来源清晰,调用者命名

两次调用创建两份独立的 loading;组合复用不等于共享状态。若函数偷偷读取全局变量或 inject,依赖仍可能变得隐式,应在接口与文档中说明。mixin 的覆盖也遵循合并规则,并不是随机决定;难点是读代码时必须追踪多个来源。


4. 大型组件的可维护性 ​


5. TypeScript 支持 ​

Options API 并不要求把每一个 this 属性都另行声明。defineComponent 为 data、computed、methods 建立关联,上面的 this.count 就是 number;普通对象字面量缺少这层上下文,不能用它证明 Options API 本身“没有类型支持”。Options API 与 TypeScript

Composition API 的 ref(0) 可推断为 Ref<number>,computed 会从返回值推断类型;但 ref([]) 并不能知道以后要存哪一种记录,所以本例仍声明 ref<Change[]>([])。API 响应、空初值、联合类型与泛型边界也需要明确类型。Composition API 与 TypeScript


6. 何时用哪个 ​

场景推荐
状态与交互较少的展示组件两者都行,遵循项目约定
需要逻辑复用✅ Composition
TypeScript 项目✅ Composition
大型复杂组件✅ Composition
团队已熟悉 Options 与现有代码可以继续使用,无需为换语法重写有效代码
渐进迁移老项目可在 setup 中调用 composable,并返回绑定供 Options/template 使用

7. 总结 ​

  • Vue 官方没有弃用 Options API 的计划;它仍受支持
  • Composition API 便于按功能组织代码、用函数复用逻辑、表达类型关系,但仍需要清晰的接口与状态归属
  • 新项目推荐 <script setup> + Composition API
  • <script setup> 是 SFC 编译语法,主体进入每个实例的 setup;它还有 props/emits 等编译宏,不只是把文本机械包进函数

两种 API 的支持状态与混用建议见 官方 FAQ。