Composition API vs Options API
问题
Vue 3 的 Composition API 和 Options API 有什么区别?各有什么优缺点?
答案
Vue 提供两种组件编写方式:Options API(选项式)和 Composition API(组合式)。
基本对比
- Options API
- Composition API
<template>
<div>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
},
mounted() {
console.log('mounted');
}
};
</script>
<template>
<div>
<p>{{ count }}</p>
<p>{{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
onMounted(() => {
console.log('mounted');
});
</script>
核心区别
| 维度 | Options API | Composition API |
|---|---|---|
| 组织方式 | 按选项类型(data/methods/computed) | 按逻辑功能 |
| this | 需要 this 访问 | 不需要 this |
| 逻辑复用 | Mixins | Composables |
| TypeScript | 支持一般 | 完美支持 |
| 代码压缩 | 一般 | 更好 |
| 学习曲线 | 低 | 中等 |
Options API 的问题
当组件变得复杂时,Options API 会导致相关逻辑分散:
// Options API:相关逻辑分散在不同选项中
export default {
data() {
return {
// 功能 A 的数据
searchQuery: '',
searchResults: [],
// 功能 B 的数据
filters: {},
// 功能 C 的数据
pagination: { page: 1, size: 10 }
};
},
computed: {
// 功能 A 的计算属性
filteredResults() { /* ... */ },
// 功能 B 的计算属性
activeFilters() { /* ... */ }
},
methods: {
// 功能 A 的方法
search() { /* ... */ },
// 功能 B 的方法
applyFilter() { /* ... */ },
// 功能 C 的方法
changePage() { /* ... */ }
},
watch: {
// 功能 A 的侦听器
searchQuery() { /* ... */ },
// 功能 C 的侦听器
'pagination.page'() { /* ... */ }
}
};
Composition API 的优势
按逻辑功能组织代码:
// Composition API:相关逻辑放在一起
import { ref, computed, watch } from 'vue';
// 功能 A:搜索
function useSearch() {
const searchQuery = ref('');
const searchResults = ref([]);
const filteredResults = computed(() => {
// 过滤逻辑
});
function search() {
// 搜索逻辑
}
watch(searchQuery, () => {
// 监听逻辑
});
return { searchQuery, searchResults, filteredResults, search };
}
// 功能 B:过滤
function useFilters() {
const filters = ref({});
const activeFilters = computed(() => { /* ... */ });
function applyFilter() { /* ... */ }
return { filters, activeFilters, applyFilter };
}
// 功能 C:分页
function usePagination() {
const pagination = ref({ page: 1, size: 10 });
function changePage(page: number) { /* ... */ }
return { pagination, changePage };
}
// 组件中使用
export default {
setup() {
const { searchQuery, filteredResults, search } = useSearch();
const { filters, applyFilter } = useFilters();
const { pagination, changePage } = usePagination();
return { searchQuery, filteredResults, search, filters, applyFilter, pagination, changePage };
}
};
逻辑复用对比
- Mixins(Options API)
- Composables(Composition API)
// mixin 定义
const mouseMixin = {
data() {
return {
x: 0,
y: 0
};
},
methods: {
update(e: MouseEvent) {
this.x = e.pageX;
this.y = e.pageY;
}
},
mounted() {
window.addEventListener('mousemove', this.update);
},
unmounted() {
window.removeEventListener('mousemove', this.update);
}
};
// 组件中使用
export default {
mixins: [mouseMixin],
// 问题1:属性来源不清晰
// 问题2:命名冲突
// 问题3:无法传参
};
// composable 定义
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e: MouseEvent) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// 组件中使用
import { useMouse } from './useMouse';
const { x, y } = useMouse();
// ✅ 来源清晰
// ✅ 可以重命名避免冲突
// ✅ 可以传参
Mixins vs Composables
| 问题 | Mixins | Composables |
|---|---|---|
| 属性来源 | 不清晰,难以追踪 | 显式导入,清晰可见 |
| 命名冲突 | 容易冲突,隐式覆盖 | 可以解构重命名 |
| 传递参数 | 不灵活 | 支持参数 |
| TypeScript | 类型推断困难 | 完美支持 |
| 逻辑组合 | 难以组合多个 mixin | 自由组合 |
TypeScript 支持对比
- Options API + TS
- Composition API + TS
import { defineComponent, PropType } from 'vue';
interface User {
id: number;
name: string;
}
export default defineComponent({
props: {
user: {
type: Object as PropType<User>,
required: true
}
},
data() {
return {
count: 0 // 类型推断为 number
};
},
computed: {
// 需要显式标注返回类型才能正确推断
doubleCount(): number {
return this.count * 2;
}
},
methods: {
// this 类型推断可能不完整
increment() {
this.count++;
}
}
});
<script setup lang="ts">
import { ref, computed } from 'vue';
interface User {
id: number;
name: string;
}
// 类型完美推断
const props = defineProps<{
user: User;
}>();
const count = ref(0); // Ref<number>
const doubleCount = computed(() => count.value * 2); // ComputedRef<number>
function increment() {
count.value++; // 完美的类型检查
}
</script>
何时使用哪种 API
| 场景 | 推荐 |
|---|---|
| 小型项目、简单组件 | Options API(更直观) |
| 大型项目、复杂组件 | Composition API(更灵活) |
| 需要逻辑复用 | Composition API |
| 使用 TypeScript | Composition API |
| Vue 2 迁移项目 | 可以混用,逐步迁移 |
常见面试问题
Q1: Composition API 相比 Options API 有什么优势?
答案:
- 更好的代码组织:将相关逻辑放在一起,而非分散在不同选项中
- 更好的逻辑复用:Composables 替代 Mixins,解决了命名冲突、来源不清等问题
- 更好的 TypeScript 支持:不依赖 this,类型推断更准确
- 更小的打包体积:变量名可以被压缩(不像 Options API 的属性名)
- 更灵活的组合:可以自由组合多个 Composables
Q2: Options API 还有存在的必要吗?
答案:
有。两种 API 会长期共存:
- 学习曲线更低:对初学者更友好
- 简单场景够用:小型组件用 Options API 更直观
- 迁移成本:大量 Vue 2 项目仍在使用
- 混用支持:Vue 3 允许在同一组件中混用
// Vue 3 中混用两种 API
export default {
data() {
return { count: 0 };
},
setup() {
const name = ref('Vue');
return { name };
}
};
Q3: setup 中为什么不能使用 this?
答案:
setup 在组件实例创建之前执行,此时 this 还不存在:
export default {
setup() {
// setup 执行顺序:
// 1. props 解析
// 2. setup() 执行 ← 当前位置
// 3. data/computed/methods 等选项处理
// 4. beforeCreate
// 5. created
console.log(this); // undefined
return {};
}
};
Composition API 设计上就不依赖 this,通过显式导入获取功能。
Q4: Composables 和 Hooks 有什么关系?
答案:
Vue 的 Composables 受 React Hooks 启发,但有本质区别:
| 特性 | Vue Composables | React Hooks |
|---|---|---|
| 执行次数 | 只在 setup 时执行一次 | 每次渲染都执行 |
| 依赖追踪 | 自动(响应式系统) | 手动声明(deps 数组) |
| 条件调用 | ✅ 可以 | ❌ 不可以 |
| 调用顺序 | 不重要 | 必须固定 |
// Vue:setup 只执行一次
setup() {
// 条件调用 OK
if (condition) {
useSomething();
}
}
// React:每次渲染都执行,顺序必须固定
function App() {
// ❌ 条件调用会报错
if (condition) {
useSomething(); // Error!
}
}
Q5: 如何从 Options API 迁移到 Composition API?
答案:
- 渐进式迁移:不需要一次性全部迁移
// 步骤 1:添加 setup,逐步迁移逻辑
export default {
data() {
return { oldData: 'value' }; // 保留旧代码
},
setup() {
const newData = ref('value'); // 新代码
return { newData };
}
};
// 步骤 2:使用 script setup 重写
<script setup lang="ts">
const newData = ref('value');
const oldData = ref('value');
</script>
- 提取 Composables:将可复用逻辑提取为组合式函数
// 提取 mixin 为 composable
// 旧:mixins: [fetchMixin]
// 新:const { data, loading, error } = useFetch(url);