Vue 3 的 Composition API 是其最重要的特性之一,它彻底改变了 Vue 组件的组织方式和逻辑复用机制。本文将深入解析 Composition API 的设计原理、核心特性与高级用法,帮助你掌握现代 Vue 开发的精髓。
1. Composition API 设计原理
1.1 为什么需要 Composition API?
在 Vue 2 中,Options API 是组件组织的主要方式,但它存在一些局限性:
- 逻辑复用困难:只能通过 mixins、高阶组件和 renderless 组件实现逻辑复用,这些方法都有各自的问题
- 代码可读性差:相关逻辑被分散到不同的选项中,导致“选项地狱”
- TypeScript 支持有限:选项式 API 的类型推断不够完善
Composition API 的出现正是为了解决这些问题:
- 逻辑复用更简单:通过组合函数实现逻辑复用,更加灵活和清晰
- 代码组织更合理:相关逻辑可以组织在一起,提高代码可读性
- TypeScript 支持更好:基于函数的 API 设计天然适合 TypeScript
1.2 Composition API 的核心概念
Composition API 的核心概念包括:
- 组合函数:使用
setup()函数或<script setup>语法组织组件逻辑 - 响应式系统:使用
ref()、reactive()等 API 创建响应式状态 - 生命周期钩子:使用
onMounted()、onUpdated()等函数式钩子 - 依赖注入:使用
provide()和inject()实现组件间通信
1.3 Composition API vs Options API
| 特性 | Composition API | Options API |
|---|---|---|
| 逻辑组织 | 按功能组织 | 按选项组织 |
| 逻辑复用 | 组合函数 | Mixins、HOC、Renderless |
| TypeScript 支持 | 优秀 | 有限 |
| 代码可读性 | 高(相关逻辑集中) | 低(逻辑分散) |
| 学习曲线 | 中等 | 低 |
2. 响应式系统深度解析
2.1 响应式系统的工作原理
Vue 3 的响应式系统使用 Proxy 替代了 Vue 2 的 Object.defineProperty,带来了以下优势:
- 支持数组索引和长度的监听
- 支持新增属性的监听
- 支持 Map、Set、WeakMap、WeakSet 的监听
- 性能更好
// Vue 3 响应式系统的核心实现原理
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 依赖收集
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 触发更新
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
},
// 其他拦截器...
});
}
2.2 ref vs reactive
ref 和 reactive 是创建响应式状态的两种主要方式:
- ref:创建一个包装对象,适用于基本类型和对象
- reactive:创建一个响应式对象,适用于对象
import { ref, reactive, toRefs, isRef, unref } from 'vue';
// 使用 ref
const count = ref(0);
console.log(count.value); // 0
count.value = 1; // 触发更新
// 使用 reactive
const state = reactive({ count: 0 });
console.log(state.count); // 0
state.count = 1; // 触发更新
// 将 reactive 对象转换为 ref
const refs = toRefs(state);
console.log(refs.count.value); // 1
// 检查是否是 ref
console.log(isRef(count)); // true
// 自动解包 ref
const value = unref(count); // 等同于 count.value
2.3 响应式系统的高级用法
- shallowRef 和 shallowReactive:创建浅层响应式对象
- readonly:创建只读响应式对象
- markRaw:标记对象为非响应式
- customRef:创建自定义 ref
import {
shallowRef,
shallowReactive,
readonly,
markRaw,
customRef
} from 'vue';
// 浅层响应式 ref
const shallow = shallowRef({ nested: { count: 0 } });
shallow.value.nested.count = 1; // 不会触发更新
shallow.value = { nested: { count: 2 } }; // 会触发更新
// 浅层响应式对象
const shallowObj = shallowReactive({ nested: { count: 0 } });
shallowObj.nested.count = 1; // 不会触发更新
shallowObj.nested = { count: 2 }; // 会触发更新
// 只读响应式对象
const readOnly = readonly({ count: 0 });
readOnly.count = 1; // 警告:无法修改只读对象
// 非响应式对象
const raw = markRaw({ count: 0 });
const wrapped = reactive(raw);
console.log(wrapped === raw); // true(返回原始对象)
// 自定义 ref
function useDebouncedRef(value, delay = 300) {
let timeout;
return customRef((track, trigger) => ({
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}));
}
const debouncedValue = useDebouncedRef('');
3. Composition API 高级用法
3.1 setup() 函数
setup() 函数是 Composition API 的入口点,在组件创建前执行:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
// 创建响应式状态
const count = ref(0);
// 定义方法
const increment = () => {
count.value++;
};
// 返回给模板使用的状态和方法
return {
count,
increment
};
}
};
</script>
3.2 <script setup> 语法糖
<script setup> 是 Vue 3.2+ 引入的语法糖,它简化了 Composition API 的使用:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 直接声明的状态和方法会自动暴露给模板
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
3.3 组合函数
组合函数是 Composition API 的核心,它允许我们将逻辑封装和复用:
// composables/useCounter.ts
import { ref, computed, onMounted } from 'vue';
export function useCounter(initialValue = 0, step = 1) {
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
const increment = () => {
count.value += step;
};
const decrement = () => {
count.value -= step;
};
const reset = () => {
count.value = initialValue;
};
// 生命周期钩子
onMounted(() => {
console.log('Counter mounted');
});
return {
count,
doubleCount,
increment,
decrement,
reset
};
}
// 在组件中使用
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup>
import { useCounter } from '@/composables/useCounter';
const { count, doubleCount, increment, decrement, reset } = useCounter(10, 2);
</script>
3.4 依赖注入
使用 provide() 和 inject() 实现组件间通信:
<!-- 父组件 -->
<template>
<div>
<h1>Parent Component</h1>
<ChildComponent />
</div>
</template>
<script setup>
import { provide, ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
// 提供状态
const theme = ref('light');
const updateTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
provide('theme', theme);
provide('updateTheme', updateTheme);
</script>
<!-- 子组件 -->
<template>
<div :class="theme">
<h2>Child Component</h2>
<p>Theme: {{ theme }}</p>
<button @click="updateTheme">Toggle Theme</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
// 注入状态
const theme = inject('theme');
const updateTheme = inject('updateTheme');
</script>
<style scoped>
.light {
background-color: white;
color: black;
}
.dark {
background-color: black;
color: white;
}
</style>
4. 生命周期钩子
4.1 生命周期钩子对照表
| Vue 2 钩子 | Vue 3 组合式 API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
| errorCaptured | onErrorCaptured |
| renderTracked | onRenderTracked |
| renderTriggered | onRenderTriggered |
4.2 生命周期钩子的使用
<template>
<div>
<h1>LifeCycle Demo</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';
const count = ref(0);
const increment = () => count.value++;
console.log('setup()');
onBeforeMount(() => {
console.log('onBeforeMount()');
});
onMounted(() => {
console.log('onMounted()');
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate()');
});
onUpdated(() => {
console.log('onUpdated()');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount()');
});
onUnmounted(() => {
console.log('onUnmounted()');
});
</script>
5. 高级组合函数
5.1 异步组合函数
// composables/useAsyncData.ts
import { ref, computed, onMounted } from 'vue';
export function useAsyncData<T>(fetchFn: () => Promise<T>) {
const data = ref<T | null>(null);
const loading = ref(true);
const error = ref<Error | null>(null);
const execute = async () => {
loading.value = true;
error.value = null;
try {
data.value = await fetchFn();
} catch (err) {
error.value = err as Error;
} finally {
loading.value = false;
}
};
onMounted(execute);
return {
data,
loading,
error,
execute
};
}
// 在组件中使用
<template>
<div>
<h1>Async Data Demo</h1>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<h2>{{ data.title }}</h2>
<p>{{ data.body }}</p>
</div>
<button @click="execute">Refresh</button>
</div>
</template>
<script setup>
import { useAsyncData } from '@/composables/useAsyncData';
interface Post {
id: number;
title: string;
body: string;
}
const { data, loading, error, execute } = useAsyncData<Post>(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
if (!response.ok) {
throw new Error('Failed to fetch post');
}
return response.json();
});
</script>
5.2 表单处理组合函数
// composables/useForm.ts
import { ref, computed } from 'vue';
export function useForm<T extends Record<string, any>>(initialValues: T, validate?: (values: T) => Record<string, string>) {
const values = ref<T>({ ...initialValues });
const errors = ref<Record<string, string>>({});
const touched = ref<Record<string, boolean>>({});
const isValid = computed(() => {
return Object.keys(errors.value).length === 0;
});
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement;
const { name, value, type, checked } = target;
values.value[name as keyof T] = type === 'checkbox' ? checked : value;
touched.value[name] = true;
if (validate) {
errors.value = validate(values.value);
}
};
const handleSubmit = (onSubmit: (values: T) => void) => {
// 标记所有字段为 touched
Object.keys(values.value).forEach(key => {
touched.value[key] = true;
});
// 验证
if (validate) {
errors.value = validate(values.value);
}
// 提交
if (isValid.value) {
onSubmit(values.value);
}
};
const reset = () => {
values.value = { ...initialValues };
errors.value = {};
touched.value = {};
};
return {
values,
errors,
touched,
isValid,
handleChange,
handleSubmit,
reset
};
}
// 在组件中使用
<template>
<form @submit.prevent="handleSubmit(onSubmit)">
<div>
<label for="name">Name:</label>
<input
id="name"
name="name"
type="text"
:value="values.name"
@change="handleChange"
/>
<p v-if="touched.name && errors.name" class="error">
{{ errors.name }}
</p>
</div>
<div>
<label for="email">Email:</label>
<input
id="email"
name="email"
type="email"
:value="values.email"
@change="handleChange"
/>
<p v-if="touched.email && errors.email" class="error">
{{ errors.email }}
</p>
</div>
<button type="submit" :disabled="!isValid">Submit</button>
<button type="button" @click="reset">Reset</button>
</form>
</template>
<script setup>
import { useForm } from '@/composables/useForm';
interface FormValues {
name: string;
email: string;
}
const { values, errors, touched, isValid, handleChange, handleSubmit, reset } = useForm<FormValues>(
{ name: '', email: '' },
(values) => {
const errors: Record<string, string> = {};
if (!values.name) {
errors.name = 'Name is required';
}
if (!values.email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = 'Email is invalid';
}
return errors;
}
);
const onSubmit = (values: FormValues) => {
console.log('Form submitted:', values);
alert('Form submitted successfully!');
};
</script>
<style scoped>
.error {
color: red;
font-size: 12px;
}
</style>
5.3 键盘事件组合函数
// composables/useKeyPress.ts
import { onMounted, onUnmounted } from 'vue';
export function useKeyPress(targetKey: string, callback: () => void) {
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === targetKey) {
callback();
}
};
onMounted(() => {
window.addEventListener('keydown', handleKeyPress);
});
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyPress);
});
}
// 在组件中使用
<template>
<div>
<h1>Key Press Demo</h1>
<p>Press the Escape key to toggle the modal</p>
<div v-if="isModalOpen" class="modal">
<div class="modal-content">
<h2>Modal</h2>
<p>This is a modal. Press Escape to close it.</p>
<button @click="closeModal">Close</button>
</div>
</div>
<button @click="openModal">Open Modal</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useKeyPress } from '@/composables/useKeyPress';
const isModalOpen = ref(false);
const openModal = () => {
isModalOpen.value = true;
};
const closeModal = () => {
isModalOpen.value = false;
};
// 使用组合函数监听 Escape 键
useKeyPress('Escape', () => {
if (isModalOpen.value) {
closeModal();
}
});
</script>
<style scoped>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 4px;
}
</style>
6. 性能优化
6.1 响应式优化
- 使用
shallowRef和shallowReactive:对于不需要深度响应的对象 - 使用
readonly:对于只读的数据 - 使用
markRaw:对于不需要响应式的对象,如第三方库实例
6.2 渲染优化
- 使用
v-memo:缓存渲染结果 - 使用
v-once:只渲染一次 - 使用
keep-alive:缓存组件实例 - 使用
defineAsyncComponent:异步加载组件
6.3 组合函数优化
- 使用
watchEffect和watch的清理函数:避免内存泄漏 - 使用
onUnmounted:清理副作用 - 合理使用
computed:缓存计算结果
<template>
<div>
<h1>Performance Optimization</h1>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
</div>
</template>
<script setup>
import { ref, computed, watchEffect, onUnmounted } from 'vue';
const count = ref(0);
// 使用 computed 缓存计算结果
const doubleCount = computed(() => {
console.log('Computing double count');
return count.value * 2;
});
// 使用 watchEffect 监听变化
watchEffect(() => {
console.log('Count changed:', count.value);
// 清理函数
return () => {
console.log('Cleaning up watcher');
};
});
// 模拟定时器
const timer = setInterval(() => {
count.value++;
}, 1000);
// 清理定时器
onUnmounted(() => {
clearInterval(timer);
});
</script>
7. 实战案例:电商购物车
7.1 功能需求
- 添加商品到购物车
- 从购物车移除商品
- 调整商品数量
- 计算总价
- 本地存储
7.2 实现
<template>
<div class="shopping-cart">
<h1>Shopping Cart</h1>
<!-- 商品列表 -->
<div class="products">
<h2>Products</h2>
<div class="product-list">
<div v-for="product in products" :key="product.id" class="product">
<h3>{{ product.name }}</h3>
<p>Price: ¥{{ product.price }}</p>
<button @click="addProduct(product)">Add to Cart</button>
</div>
</div>
</div>
<!-- 购物车 -->
<div class="cart">
<h2>Cart</h2>
<div v-if="cartItems.length === 0" class="empty-cart">
Your cart is empty
</div>
<div v-else class="cart-items">
<div v-for="item in cartItems" :key="item.id" class="cart-item">
<h3>{{ item.name }}</h3>
<p>Price: ¥{{ item.price }}</p>
<div class="quantity-control">
<button @click="decreaseQuantity(item.id)">-</button>
<span>{{ item.quantity }}</span>
<button @click="increaseQuantity(item.id)">+</button>
</div>
<button @click="removeItem(item.id)" class="remove-btn">Remove</button>
</div>
<div class="cart-summary">
<h3>Total: ¥{{ totalPrice }}</h3>
<button class="checkout-btn">Checkout</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
interface Product {
id: number;
name: string;
price: number;
}
interface CartItem extends Product {
quantity: number;
}
// 模拟商品数据
const products = ref<Product[]>([
{ id: 1, name: 'iPhone 15', price: 6999 },
{ id: 2, name: 'MacBook Pro', price: 14999 },
{ id: 3, name: 'AirPods Pro', price: 1999 }
]);
// 购物车数据
const cartItems = ref<CartItem[]>([]);
// 计算总价
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
// 添加商品到购物车
const addProduct = (product: Product) => {
const existingItem = cartItems.value.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
cartItems.value.push({
...product,
quantity: 1
});
}
saveCart();
};
// 从购物车移除商品
const removeItem = (id: number) => {
cartItems.value = cartItems.value.filter(item => item.id !== id);
saveCart();
};
// 增加商品数量
const increaseQuantity = (id: number) => {
const item = cartItems.value.find(item => item.id === id);
if (item) {
item.quantity++;
saveCart();
}
};
// 减少商品数量
const decreaseQuantity = (id: number) => {
const item = cartItems.value.find(item => item.id === id);
if (item) {
if (item.quantity > 1) {
item.quantity--;
} else {
removeItem(id);
}
saveCart();
}
};
// 保存购物车到本地存储
const saveCart = () => {
localStorage.setItem('cart', JSON.stringify(cartItems.value));
};
// 从本地存储加载购物车
const loadCart = () => {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
cartItems.value = JSON.parse(savedCart);
}
};
// 生命周期钩子
onMounted(() => {
loadCart();
});
</script>
<style scoped>
.shopping-cart {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.products, .cart {
margin: 40px 0;
}
.product-list, .cart-items {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.product, .cart-item {
border: 1px solid #ccc;
padding: 20px;
border-radius: 4px;
width: 250px;
}
.quantity-control {
display: flex;
align-items: center;
gap: 10px;
margin: 10px 0;
}
.remove-btn {
margin-top: 10px;
background-color: #ff4757;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.cart-summary {
margin-top: 20px;
padding: 20px;
border-top: 1px solid #ccc;
}
.checkout-btn {
background-color: #2ed573;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.empty-cart {
padding: 40px;
text-align: center;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>
8. 最佳实践总结
8.1 代码组织
- 使用组合函数:将相关逻辑封装到组合函数中
- 按功能组织文件:将相关的组件、组合函数和工具放在一起
- 使用 TypeScript:添加类型安全
- 使用 ESLint 和 Prettier:保持代码风格一致
8.2 响应式状态管理
- 优先使用
ref:对于基本类型和需要在模板中直接使用的状态 - 使用
reactive:对于复杂对象 - 使用
computed:对于派生状态 - 使用
watch和watchEffect:对于副作用
8.3 组件设计
- 使用
<script setup>:简化 Composition API 的使用 - 合理使用 props 和 emit:实现组件间通信
- 使用
provide和inject:实现跨层级组件通信 - 使用
defineAsyncComponent:异步加载组件
8.4 性能优化
- 避免不必要的响应式:使用
shallowRef、shallowReactive、readonly、markRaw - 优化渲染:使用
v-memo、v-once、keep-alive - 清理副作用:使用
onUnmounted和 watch 清理函数 - 合理使用计算属性:缓存计算结果
8.5 测试
- 测试组合函数:测试逻辑的正确性
- 测试组件:测试组件的渲染和交互
- 使用 Vitest:Vue 3 推荐的测试框架
9. 未来展望
9.1 Vue 3 的发展
Vue 3 自发布以来,已经成为 Vue 生态系统的主流版本,它的 Composition API 也被广泛采用。未来,Vue 团队将继续改进 Composition API,使其更加完善和易用。
9.2 生态系统
Vue 3 的生态系统正在不断完善:
- Pinia:Vue 3 官方推荐的状态管理库
- Vue Router 4:Vue 3 兼容的路由库
- Vite:Vue 3 官方推荐的构建工具
- Nuxt 3:基于 Vue 3 的 SSR 框架
9.3 学习资源
- 官方文档:https://v3.vuejs.org/
- Vue Mastery:https://www.vuemastery.com/
- Vue School:https://vueschool.io/
- Awesome Vue:https://github.com/vuejs/awesome-vue
总结
Vue 3 的 Composition API 是一项重大的技术革新,它为 Vue 开发者提供了一种更灵活、更强大的组件组织方式。通过本文的学习,你应该已经掌握了 Composition API 的核心概念、设计原理和高级用法。
Composition API 的优势在于:
- 更好的逻辑组织:相关逻辑可以集中在一起
- 更简单的逻辑复用:通过组合函数实现
- 更好的 TypeScript 支持:基于函数的 API 设计
- 更好的性能:响应式系统的优化
随着 Vue 3 和 Composition API 的不断发展,它们将成为前端开发的重要工具。通过不断学习和实践,你可以构建出更加高质量、可维护的 Vue 应用。