引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比于传统的 Options API,Composition API 提供了更灵活、更强大的组件逻辑复用方式,极大地提升了代码的可维护性和开发效率。本文将深入探讨 Vue 3 Composition API 的核心概念、使用技巧以及在实际项目中的最佳实践。
Vue 3 Composition API 核心概念
什么是 Composition API
Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。它允许开发者以函数的形式组织和复用组件逻辑,而不再局限于传统的选项(options)形式。通过 Composition API,我们可以将相关的逻辑代码组合在一起,使组件更加清晰和易于维护。
与 Options API 的对比
在 Vue 2 中,我们通常使用 data、methods、computed、watch 等选项来组织组件逻辑。这种方式在小型组件中表现良好,但在大型复杂组件中容易导致逻辑分散,难以维护。
// Vue 2 Options API 示例
export default {
data() {
return {
count: 0,
message: ''
}
},
computed: {
doubledCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
}
},
watch: {
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`);
}
}
}
而使用 Composition API,我们可以将相关的逻辑组合在一起:
// Vue 3 Composition API 示例
import { ref, computed, watch } from 'vue';
export default {
setup() {
const count = ref(0);
const message = ref('');
const doubledCount = computed(() => count.value * 2);
const increment = () => {
count.value++;
};
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`);
});
return {
count,
message,
doubledCount,
increment
}
}
}
响应式系统详解
ref 和 reactive 的使用
在 Composition API 中,响应式系统是核心概念之一。ref 和 reactive 是两个主要的响应式API:
import { ref, reactive } from 'vue';
// 使用 ref 创建响应式变量
const count = ref(0);
const message = ref('Hello World');
// 使用 reactive 创建响应式对象
const state = reactive({
name: 'Vue',
version: 3,
features: ['Composition API', 'Better Performance']
});
// 访问和修改值
console.log(count.value); // 0
count.value = 1; // 修改值
console.log(state.name); // Vue
state.name = 'Vue 3'; // 修改对象属性
响应式数据的解构问题
需要注意的是,当从 reactive 对象中解构出属性时,会失去响应性:
import { reactive } from 'vue';
const state = reactive({
count: 0,
message: 'Hello'
});
// ❌ 错误:解构会丢失响应性
const { count, message } = state;
// ✅ 正确:使用 ref 或保持对象引用
const countRef = ref(state.count);
const messageRef = ref(state.message);
// 或者直接使用原始对象
const getCount = () => state.count;
computed 的高级用法
computed 函数支持更复杂的计算逻辑:
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
const age = ref(30);
// 基本计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 带有 getter 和 setter 的计算属性
const displayName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const names = value.split(' ');
firstName.value = names[0];
lastName.value = names[1];
}
});
// 复杂计算
const userStatus = computed(() => {
if (age.value < 18) return 'minor';
if (age.value < 65) return 'adult';
return 'senior';
});
return {
firstName,
lastName,
age,
fullName,
displayName,
userStatus
}
}
}
组件通信实战
父子组件通信
在 Composition API 中,父子组件通信依然遵循 Vue 的设计原则:
<!-- Parent.vue -->
<template>
<div>
<h2>Parent Component</h2>
<Child
:message="parentMessage"
:count="count"
@child-event="handleChildEvent"
/>
<button @click="increment">Increment: {{ count }}</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentMessage = ref('Hello from Parent');
const count = ref(0);
const increment = () => {
count.value++;
};
const handleChildEvent = (data) => {
console.log('Received from child:', data);
};
</script>
<!-- Child.vue -->
<template>
<div>
<h3>Child Component</h3>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<button @click="sendToParent">Send to Parent</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
message: String,
count: Number
});
const emit = defineEmits(['child-event']);
const sendToParent = () => {
emit('child-event', {
timestamp: Date.now(),
data: 'Hello from child'
});
};
</script>
兄弟组件通信
使用事件总线或全局状态管理来实现兄弟组件通信:
<!-- ComponentA.vue -->
<template>
<div>
<h3>Component A</h3>
<button @click="sendData">Send Data</button>
</div>
</template>
<script setup>
import { inject } from 'vue';
const eventBus = inject('eventBus');
const sendData = () => {
eventBus.emit('data-sent', {
message: 'Hello from Component A',
timestamp: Date.now()
});
};
</script>
<!-- ComponentB.vue -->
<template>
<div>
<h3>Component B</h3>
<p>Received: {{ receivedData }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const receivedData = ref('');
const eventBus = inject('eventBus');
onMounted(() => {
eventBus.on('data-sent', (data) => {
receivedData.value = data.message;
});
});
onUnmounted(() => {
eventBus.off('data-sent');
});
</script>
状态管理最佳实践
使用 Pinia 进行状态管理
Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API 和更好的 TypeScript 支持:
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref(null);
const isLoggedIn = ref(false);
// 计算属性
const userName = computed(() => user.value?.name || '');
const userRole = computed(() => user.value?.role || 'guest');
// 方法
const login = (userData) => {
user.value = userData;
isLoggedIn.value = true;
};
const logout = () => {
user.value = null;
isLoggedIn.value = false;
};
const updateProfile = (profileData) => {
if (user.value) {
user.value = { ...user.value, ...profileData };
}
};
return {
user,
isLoggedIn,
userName,
userRole,
login,
logout,
updateProfile
};
});
<!-- UserComponent.vue -->
<template>
<div>
<div v-if="isLoggedIn">
<p>Welcome, {{ userName }}!</p>
<p>Role: {{ userRole }}</p>
<button @click="logout">Logout</button>
</div>
<div v-else>
<button @click="login">Login</button>
</div>
</div>
</template>
<script setup>
import { useUserStore } from '@/stores/user';
import { onMounted } from 'vue';
const userStore = useUserStore();
const login = () => {
userStore.login({
name: 'John Doe',
role: 'admin'
});
};
const logout = () => {
userStore.logout();
};
onMounted(() => {
// 初始化用户状态
if (localStorage.getItem('user')) {
const userData = JSON.parse(localStorage.getItem('user'));
userStore.login(userData);
}
});
</script>
复杂状态管理示例
// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
// 状态
const items = ref([]);
// 计算属性
const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0));
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => {
return sum + (item.price * item.quantity);
}, 0);
});
const isEmpty = computed(() => items.value.length === 0);
// 方法
const addItem = (product) => {
const existingItem = items.value.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += 1;
} else {
items.value.push({
...product,
quantity: 1
});
}
};
const removeItem = (productId) => {
items.value = items.value.filter(item => item.id !== productId);
};
const updateQuantity = (productId, quantity) => {
const item = items.value.find(item => item.id === productId);
if (item) {
if (quantity <= 0) {
removeItem(productId);
} else {
item.quantity = quantity;
}
}
};
const clearCart = () => {
items.value = [];
};
// 持久化存储
const loadFromStorage = () => {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
items.value = JSON.parse(savedCart);
}
};
const saveToStorage = () => {
localStorage.setItem('cart', JSON.stringify(items.value));
};
// 监听状态变化
const watch = () => {
items.value = items.value;
saveToStorage();
};
return {
items,
totalItems,
totalPrice,
isEmpty,
addItem,
removeItem,
updateQuantity,
clearCart,
loadFromStorage,
saveToStorage,
watch
};
});
组件逻辑复用
自定义 Composable 函数
Composable 是 Vue 3 中用于复用逻辑的函数,它们以 use 开头命名:
// composables/useFetch.js
import { ref, watch } from 'vue';
export function useFetch(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
// 立即执行
fetchData();
// 监听 url 变化
watch(url, fetchData);
return {
data,
loading,
error,
refetch: fetchData
};
}
<!-- DataComponent.vue -->
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<h2>{{ data?.title }}</h2>
<p>{{ data?.body }}</p>
<button @click="refetch">Refresh</button>
</div>
</div>
</template>
<script setup>
import { useFetch } from '@/composables/useFetch';
const url = 'https://jsonplaceholder.typicode.com/posts/1';
const { data, loading, error, refetch } = useFetch(url);
</script>
复杂逻辑复用
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key);
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
return value;
}
<!-- SettingsComponent.vue -->
<template>
<div>
<h3>Settings</h3>
<div>
<label>
Theme:
<select v-model="settings.theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label>
Language:
<select v-model="settings.language">
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</label>
</div>
</div>
</template>
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage';
const settings = useLocalStorage('user-settings', {
theme: 'light',
language: 'en'
});
</script>
生命周期钩子
setup 函数的使用
setup 是 Composition API 的入口函数,它在组件实例创建之前执行:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
// 生命周期钩子
onMounted(() => {
console.log('Component mounted');
// 初始化逻辑
});
onUpdated(() => {
console.log('Component updated');
// 更新后的逻辑
});
onUnmounted(() => {
console.log('Component unmounted');
// 清理工作
});
</script>
异步操作处理
<script setup>
import { ref, onMounted } from 'vue';
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
try {
loading.value = true;
const response = await fetch('/api/data');
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchData();
});
</script>
TypeScript 集成
类型定义最佳实践
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export interface UserProfile {
avatar: string;
bio: string;
preferences: {
theme: 'light' | 'dark';
language: string;
};
}
<!-- TypedComponent.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { User, UserProfile } from '@/types/user';
const user = ref<User | null>(null);
const profile = ref<UserProfile | null>(null);
const userName = computed(() => user.value?.name || '');
const userEmail = computed(() => user.value?.email || '');
// 定义 props 类型
interface Props {
title?: string;
showAvatar?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
showAvatar: false
});
// 定义 emits 类型
type Emits = {
(e: 'update:user', user: User): void;
(e: 'error', error: Error): void;
};
const emit = defineEmits<Emits>();
</script>
性能优化技巧
计算属性缓存
// 正确的计算属性使用方式
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('');
const lastName = ref('');
// 复杂计算属性,会自动缓存
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 带有依赖的复杂计算
const expensiveCalculation = computed(() => {
// 模拟耗时操作
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += i;
}
return result;
});
return {
firstName,
lastName,
fullName,
expensiveCalculation
};
}
};
避免不必要的响应式
// ❌ 不推荐:不必要的响应式
const data = ref({
name: 'John',
age: 30,
// 这些数据不需要响应式
config: {
theme: 'light',
language: 'en'
}
});
// ✅ 推荐:合理使用响应式
const user = ref({
name: 'John',
age: 30
});
const config = {
theme: 'light',
language: 'en'
};
实际项目案例
完整的购物车应用示例
<!-- ShoppingCart.vue -->
<template>
<div class="shopping-cart">
<h2>Shopping Cart</h2>
<div v-if="cartStore.isEmpty" class="empty-cart">
Your cart is empty
</div>
<div v-else>
<div
v-for="item in cartStore.items"
:key="item.id"
class="cart-item"
>
<img :src="item.image" :alt="item.name" class="item-image">
<div class="item-info">
<h3>{{ item.name }}</h3>
<p class="price">¥{{ item.price }}</p>
<div class="quantity-controls">
<button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
<span>{{ item.quantity }}</span>
<button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
</div>
</div>
<button @click="removeItem(item.id)" class="remove-btn">Remove</button>
</div>
<div class="cart-summary">
<h3>Order Summary</h3>
<div class="summary-row">
<span>Total Items:</span>
<span>{{ cartStore.totalItems }}</span>
</div>
<div class="summary-row">
<span>Total Price:</span>
<span class="total-price">¥{{ cartStore.totalPrice.toFixed(2) }}</span>
</div>
<button @click="checkout" class="checkout-btn">Checkout</button>
</div>
</div>
</div>
</template>
<script setup>
import { useCartStore } from '@/stores/cart';
import { onMounted } from 'vue';
const cartStore = useCartStore();
const updateQuantity = (productId, quantity) => {
cartStore.updateQuantity(productId, quantity);
};
const removeItem = (productId) => {
cartStore.removeItem(productId);
};
const checkout = () => {
// 模拟结账逻辑
alert(`Checkout complete! Total: ¥${cartStore.totalPrice.toFixed(2)}`);
cartStore.clearCart();
};
onMounted(() => {
cartStore.loadFromStorage();
});
</script>
<style scoped>
.shopping-cart {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.cart-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
}
.item-image {
width: 80px;
height: 80px;
object-fit: cover;
margin-right: 15px;
}
.item-info {
flex: 1;
}
.quantity-controls {
display: flex;
align-items: center;
margin-top: 10px;
}
.quantity-controls button {
width: 30px;
height: 30px;
margin: 0 5px;
}
.remove-btn {
background-color: #ff4757;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
}
.cart-summary {
margin-top: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 5px;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.total-price {
font-weight: bold;
color: #ff6b6b;
}
.checkout-btn {
width: 100%;
padding: 12px;
background-color: #48dbfb;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
margin-top: 15px;
}
</style>
总结
Vue 3 Composition API 为前端开发带来了革命性的变化,它提供了更灵活的组件逻辑组织方式和强大的复用能力。通过本文的详细介绍,我们看到了:
- 响应式系统的深入理解:正确使用
ref和reactive创建响应式数据 - 组件通信的最佳实践:父子、兄弟组件间的有效通信
- 状态管理的完整方案:结合 Pinia 实现复杂的状态管理
- 逻辑复用的强大能力:通过自定义 Composable 函数实现代码复用
- 性能优化策略:合理使用计算属性和避免不必要的响应式
- TypeScript 集成:提供完整的类型安全支持
在实际项目中,建议根据具体需求选择合适的 API 形式。对于简单的组件,Options API 依然简单易用;而对于复杂的逻辑复用场景,Composition API 显得更加灵活和强大。
通过掌握这些技术要点,开发者可以构建出更加健壮、可维护的 Vue 应用程序,同时提升开发效率和代码质量。随着 Vue 生态的不断发展,Composition API 将成为现代前端开发的重要工具。

评论 (0)