D01 · Options API vs Composition API
对应主课: L37 Composition API 设计哲学 最后核对: 2026-09-23
1. 核心区别
| 维度 | Options API | Composition API |
|---|---|---|
| 代码组织 | 按选项类型分组 (data/computed/methods) | 按功能逻辑分组 |
| 逻辑复用 | mixin、工具函数、组件组合;也可通过 setup 使用 composable | 用函数显式传入依赖、返回状态与操作 |
| TypeScript | defineComponent 支持推断,复杂 mixin 合并较难表达 | 普通变量/函数便于推断,仍需为边界和泛型声明类型 |
| this 依赖 | 依赖 this 上下文 | 不依赖 this |
| 组织约束 | 选项提供固定放置位置 | 需要自己划分函数与模块边界 |
| 产物差异 | 需要 Options 处理逻辑与实例代理访问 | script setup 有利于直接访问绑定与压缩;最终体积取决于实际构建 |
切换语法不会自动让应用代码都可 tree-shake。若关闭 __VUE_OPTIONS_API__ 才能排除相应运行时支持,还必须确认依赖组件也不使用 Options API;普通课程应用保持默认开启。编译标志
2. 同一功能的两种写法
这两个计数器行为相同:增加、限制非负的减少、显示派生值、记录每次变化,重置后历史为空。默认 watcher 会批处理;这里刻意用同步 watcher 记录标量计数的每次修改,reset 先触发记录再清空历史,不会在下一次刷新时重新补出一条记录。
Options API
<!-- 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
<!-- 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 项目中导入两个组件,放到同一个实验页面。它们各自持有状态,点击其中一个不会影响另一个:
<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 名称:
// 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 显式表达返回值
// 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 }
}// 组件的 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。