Composition API 解决的问题
Vue 2 的选项式 API(Options API) 以数据(data)、方法(methods)、计算属性(computed)、生命周期钩子(hooks)为组织代码的单位。当组件逻辑简单时,这种组织方式清晰直观——你知道自己应该去哪里找某个功能的代码。
但当组件变得复杂时,问题出现了:
- 逻辑关注点分散:同一个功能的代码散落在
data、methods、computed、watch四个选项中 - 代码复用困难:mixins 有命名冲突和隐式依赖问题,高阶组件(HOC)有嵌套地狱
- 类型推导不完美:TypeScript 支持是后期追加的,与 Vue 2 的响应式系统结合有局限
Composition API(组合式 API)的诞生,就是为了解决这些问题。它以逻辑关注点(feature)为组织代码的单位,而不是以选项类型为单位。
一、为什么 Composition API 更适合组织逻辑
1.1 选项式 API 的逻辑分散问题
// Vue 2 选项式 API:同一功能的代码分散在多处
export default {
data() {
return {
// 搜索相关的数据
searchQuery: '',
searchResults: [],
isSearching: false,
// 分页相关的数据
currentPage: 1,
pageSize: 10,
// 排序相关的数据
sortBy: 'date',
sortOrder: 'desc'
};
},
computed: {
// 搜索相关的计算属性
hasSearchResults() {
return this.searchResults.length > 0;
},
// 分页相关的计算属性
totalPages() {
return Math.ceil(this.searchResults.length / this.pageSize);
}
},
methods: {
// 搜索相关的方法
async performSearch() { /* ... */ },
// 分页相关的方法
changePage(page) { /* ... */ },
// 排序相关的方法
changeSort(field) { /* ... */ }
},
watch: {
// 搜索相关的 watcher
searchQuery() { this.performSearch(); }
}
};
在这个例子里,searchQuery、searchResults、isSearching、performSearch、hasSearchResults、watcher 都被"打散"在不同的选项中。如果你需要修改搜索功能,必须在四个选项中来回跳转。
1.2 Composition API 的逻辑内聚
<script setup>
import { ref, computed, watch } from 'vue';
// 搜索功能的逻辑内聚在一起
const searchQuery = ref('');
const searchResults = ref([]);
const isSearching = ref(false);
const hasSearchResults = computed(() => searchResults.value.length > 0);
async function performSearch() {
isSearching.value = true;
searchResults.value = await api.search(searchQuery.value);
isSearching.value = false;
}
watch(searchQuery, performSearch);
// 分页功能的逻辑内聚在一起
const currentPage = ref(1);
const pageSize = ref(10);
const totalPages = computed(() =>
Math.ceil(searchResults.value.length / pageSize.value)
);
function changePage(page) {
currentPage.value = page;
}
// 排序功能的逻辑内聚在一起
const sortBy = ref('date');
const sortOrder = ref('desc');
function changeSort(field) {
if (sortBy.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
sortBy.value = field;
sortOrder.value = 'desc';
}
}
</script>
现在,search、pagination、sort 三个功能的代码各自内聚在一起,修改任何一个功能只需要在一个代码块中完成。
二、setup 函数和 script setup
2.1 setup 函数
setup 是 Composition API 的入口函数,在组件实例创建之前执行(beforeCreate 之前)。此时组件的响应式系统已经初始化,但 DOM 还未创建。
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
// 访问 props
// setup 函数的第一个参数是 props
// 第二个参数是 context(包含 attrs、slots、emit、expose)
function increment() {
count.value++;
}
onMounted(() => {
console.log('Component mounted');
});
// 通过 return 语句暴露给模板
return {
count,
increment
};
}
};
</script>
2.2 script setup:更简洁的语法
<script setup> 是 setup 函数的语法糖。在 script setup 中,顶层代码直接在 setup 函数作用域内执行,不需要显式 return。
<script setup>
import { ref, onMounted } from 'vue';
// count 和 increment 自动暴露给模板
const count = ref(0);
function increment() {
count.value++;
}
onMounted(() => {
console.log('Component mounted');
});
// 不需要 return 语句
</script>
script setup 的优势:
- 代码更简洁
- 自动暴露给模板的变量不需要 return
- 更好的 IDE 支持(Vue Volar/JSX 插件提供更准确的类型推导)
- 略微更好的性能(不需要包装函数)
2.3 两者如何选择
- 新项目:直接用
<script setup>,它是 Vue 3 的推荐语法 - 需要与其他选项混合:用
setup函数(可以与data、methods等共存) - TypeScript 优先:
<script setup>的类型推导更完整
三、响应式系统核心:reactive/ref/computed/watch
3.1 ref:基础类型的响应式引用
ref 创建了一个引用(reference),可以包装任何值(基础类型和对象)。访问或修改值需要 .value。
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
// 在模板中自动解包,不需要 .value
// <template>{{ count }}</template> 渲染为 1
ref 的原理:创建一个包含 .value 属性的响应式对象,在模板中自动解包。
3.2 reactive:对象的深度响应式
reactive 创建一个深度响应式的对象。深层所有属性都会自动是响应式的。
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 30
}
});
state.count++;
state.user.name = 'Bob'; // 深层属性也是响应式的
ref vs reactive 的选择:
| 场景 | 推荐 |
|---|---|
| 基础类型(string、number、boolean) | ref |
| 对象类型 | reactive 或 ref |
| 数组 | ref |
| 需要替换整个响应式对象 | ref |
| 保持解构响应式 | reactive + toRefs |
3.3 computed:派生值
computed 创建基于其他响应式数据的派生值。只有当依赖变化时才会重新计算。
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 可写计算属性
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
[firstName.value, lastName.value] = value.split(' ');
}
});
3.4 watch 和 watchEffect:响应变化
watch:显式指定要监听的数据源,只有这些数据变化时才执行回调。
import { ref, watch } from 'vue';
const count = ref(0);
// 监听单个 ref
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`);
});
// 监听多个数据源
const firstName = ref('');
const lastName = ref('');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name changed: ${oldFirst} ${oldLast} → ${newFirst} ${newLast}`);
}, { immediate: true }); // immediate: true 立即执行一次
watchEffect:立即执行一次回调,追踪回调中使用的所有响应式数据,自动收集依赖。
import { ref, watchEffect } from 'vue';
const userId = ref(1);
watchEffect(() => {
// 立即执行,并自动追踪 userId.value 的依赖
console.log(`Fetching user ${userId.value}`);
fetchUser(userId.value);
});
// 当 userId.value 变化时,上面的函数会自动重新执行
watch vs watchEffect 的选择:
watch:需要显式控制监听哪些数据、访问旧值、懒执行(默认)watchEffect:需要立即执行、关心回调中使用的所有响应式数据
四、Composables:逻辑复用的新方式
4.1 什么是 Composable
Composable(组合式函数)是 Vue 3 推荐的逻辑复用模式。它是一个使用 Composition API 的函数,封装了可复用的逻辑,可以被多个组件共享。
// composables/useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function reset() {
count.value = initialValue;
}
return {
count,
double,
increment,
decrement,
reset
};
}
<script setup>
import { useCounter } from '@/composables/useCounter';
const { count, double, increment, decrement } = useCounter(10);
</script>
4.2 Composable 的命名约定
Composable 函数以 use 开头(use + 名词/名词短语),这是社区共识,让其他开发者一眼认出这是 Composition API 的逻辑复用:
useWindowSize— 获取窗口尺寸useLocalStorage— 响应式 localStorageuseFetch— 数据获取封装useDebounce— 防抖封装
4.3 Composable 的状态共享
默认情况下,每个组件调用 Composable 获得独立的响应式状态。如果需要跨组件共享状态:
// composables/useCounterStore.js
// 模块级状态:所有使用这个 Composable 的组件共享同一个状态
const count = ref(0);
export function useCounterStore() {
function increment() {
count.value++;
}
return { count, increment };
}
<!-- ComponentA.vue -->
<script setup>
import { useCounterStore } from '@/composables/useCounterStore';
const { count, increment } = useCounterStore();
</script>
<!-- ComponentB.vue -->
<script setup>
import { useCounterStore } from '@/composables/useCounterStore';
const { count, increment } = useCounterStore();
<!-- count 在两个组件间共享,改变一个,另一个也变化 -->
</script>
4.4 Composable vs React Hooks
Vue Composable 和 React Hooks 都是为了逻辑复用,但有本质区别:
| 方面 | Vue Composables | React Hooks |
|---|---|---|
| 调用时机 | 组件定义时(setup)同步调用 | 每次渲染时同步调用 |
| 条件调用 | 不能条件调用(必须在组件顶层) | 可以在条件语句中调用(但有 eslint 规则禁止) |
| 状态管理 | 每次调用创建独立状态 | 每次调用可能返回同一个状态(useRef) |
| 生命周期 | 使用 Vue 的生命周期钩子 | 使用 React 的 useEffect |
| 响应式系统 | Vue 响应式系统 | React 自己的状态系统 |
关键区别:React Hooks 有"Hooks 的规则"(不能在条件语句中使用,必须在顶层调用),因为 React 需要在渲染之间保持 Hook 的调用顺序。Vue 的 Composable 只是普通函数,不受这些限制。
五、生命周期钩子在 Composition API 中的使用
<script setup>
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue';
onBeforeMount(() => {
// 组件挂载之前调用
});
onMounted(() => {
// 组件挂载之后调用
console.log('Component mounted');
});
onBeforeUpdate(() => {
// 组件更新之前调用
});
onUpdated(() => {
// 组件更新之后调用
});
onBeforeUnmount(() => {
// 组件卸载之前调用
});
onUnmounted(() => {
// 组件卸载之后调用
console.log('Component unmounted');
});
六、Provide/Inject:跨组件通信
Composition API 中的 provide/inject 用于深层组件通信:
<!-- Parent.vue -->
<script setup>
import { provide, ref } from 'vue';
const count = ref(0);
// 提供给所有后代组件
provide('count', count);
provide('increment', () => count.value++);
</script>
<!-- Child.vue -->
<script setup>
import { inject } from 'vue';
// 从祖先组件获取
const count = inject('count');
const increment = inject('increment');
</script>
七、面试高频问题
Q1: ref 和 reactive 的区别是什么?什么时候用哪个?
ref 用于基础类型,创建包含 .value 的响应式引用,在模板中自动解包。reactive 用于对象类型,创建深度响应式对象。实践中,基础类型用 ref,对象/数组用 ref 或 reactive(取决于是否需要替换整个对象)。
Q2: Composition API 相比选项式 API 的优势?
逻辑内聚(同一功能的代码在一起)、更好的 TypeScript 支持、更灵活的逻辑复用(composables vs mixins)、更好的代码组织(复杂组件更容易维护)。
Q3: watch 和 watchEffect 的区别?
watch 需要显式指定要监听的数据源,支持旧值,默认懒执行。watchEffect 立即执行并自动追踪依赖,不需要显式指定要监听的数据。
Q4: Composable 和 mixin 的区别?
Mixin 有命名冲突(多个 mixin 可能有同名属性)、隐式依赖(不清楚 mixin 依赖哪些数据)、不清晰的来源(难以知道某个属性来自哪个 mixin)。Composable 没有这些问题:返回值清晰、显式依赖、无命名冲突。
延展阅读
- Vue 官方文档 — Composition API 介绍 — 官方 FAQ,解答了很多常见疑问
- Vue 官方文档 — script setup — script setup 详细文档
- Vue 官方文档 — Composables — Composable 最佳实践
- Vue Mastery — Vue 3 Composition API — 视频课程