引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。作为 Vue 3 的核心特性之一,Composition API 不仅提供了更灵活的代码组织方式,还为组件通信、状态管理和性能优化带来了全新的可能性。本文将深入探讨 Composition API 的核心特性,并结合实际项目经验,分享如何在真实场景中应用这些技术来构建高性能的 Vue 应用。
什么是 Composition API
核心概念
Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式,它允许我们使用函数来组织和复用组件逻辑。与传统的 Options API 相比,Composition API 更加灵活,能够更好地处理复杂的组件逻辑,特别是在需要在多个组件间共享逻辑时。
主要优势
- 更好的逻辑复用:通过组合函数实现逻辑的复用
- 更清晰的代码组织:将相关的逻辑组织在一起
- 更强的类型支持:与 TypeScript 集成更好
- 更好的开发体验:更适合大型项目的维护
组件通信实战
1. Props 传递数据
在 Composition API 中,props 的使用方式与 Options API 基本一致,但更加灵活:
// ChildComponent.vue
import { defineProps, computed } from 'vue'
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => []
}
})
// 使用计算属性处理 props
const displayTitle = computed(() => {
return `标题:${props.title}`
})
2. emit 事件通信
// ChildComponent.vue
import { defineEmits } from 'vue'
const emit = defineEmits(['updateCount', 'itemSelected'])
const handleIncrement = () => {
emit('updateCount', props.count + 1)
}
const handleItemSelect = (item) => {
emit('itemSelected', item)
}
3. provide/inject 跨层级通信
// Parent.vue
import { provide, ref } from 'vue'
const theme = ref('light')
const user = ref({ name: 'John', role: 'admin' })
provide('theme', theme)
provide('user', user)
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
// Child.vue
import { inject } from 'vue'
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
const toggleTheme = () => {
updateTheme(theme.value === 'light' ? 'dark' : 'light')
}
4. 全局状态共享
// stores/globalStore.js
import { reactive, readonly } from 'vue'
const state = reactive({
user: null,
theme: 'light',
notifications: []
})
const setUser = (userData) => {
state.user = userData
}
const setTheme = (newTheme) => {
state.theme = newTheme
}
const addNotification = (notification) => {
state.notifications.push(notification)
}
export const globalStore = {
state: readonly(state),
setUser,
setTheme,
addNotification
}
响应式数据管理
1. reactive 和 ref 的使用
import { reactive, ref, computed, watch } from 'vue'
// 使用 ref 处理基本类型
const count = ref(0)
const name = ref('Vue')
// 使用 reactive 处理对象
const userInfo = reactive({
id: 1,
name: 'John',
email: 'john@example.com',
profile: {
avatar: '',
bio: ''
}
})
// 计算属性
const doubledCount = computed(() => count.value * 2)
const displayName = computed({
get: () => userInfo.name,
set: (value) => {
userInfo.name = value
}
})
// 监听器
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
watch(userInfo, (newVal) => {
console.log('userInfo changed:', newVal)
}, { deep: true })
2. watchEffect 的高级用法
import { watchEffect, ref } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = ref('')
// watchEffect 会自动追踪依赖
watchEffect(() => {
fullName.value = `${firstName.value} ${lastName.value}`
})
// 停止监听
const stop = watchEffect(() => {
console.log('Watching:', firstName.value)
})
// 在适当时候停止
// stop()
3. 自定义组合函数
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const doubled = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
doubled
}
}
// 使用自定义组合函数
// MyComponent.vue
import { useCounter } from '@/composables/useCounter'
export default {
setup() {
const { count, increment, decrement, doubled } = useCounter(10)
return {
count,
increment,
decrement,
doubled
}
}
}
状态管理最佳实践
1. Pinia 状态管理库
// stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const user = ref(null)
const isLoggedIn = computed(() => !!user.value)
const setUser = (userData) => {
user.value = userData
}
const clearUser = () => {
user.value = null
}
const fetchUserProfile = async () => {
try {
const response = await fetch('/api/user/profile')
const userData = await response.json()
setUser(userData)
} catch (error) {
console.error('Failed to fetch user profile:', error)
}
}
return {
user,
isLoggedIn,
setUser,
clearUser,
fetchUserProfile
}
})
2. 多模块状态管理
// stores/index.js
import { createPinia } from 'pinia'
import { useUserStore } from './userStore'
import { useThemeStore } from './themeStore'
import { useNotificationStore } from './notificationStore'
const pinia = createPinia()
export { pinia, useUserStore, useThemeStore, useNotificationStore }
// 在组件中使用
// MyComponent.vue
import { useUserStore, useThemeStore } from '@/stores'
export default {
setup() {
const userStore = useUserStore()
const themeStore = useThemeStore()
const handleLogin = async () => {
await userStore.fetchUserProfile()
// 用户登录后更新主题
if (userStore.isLoggedIn) {
themeStore.setTheme('dark')
}
}
return {
userStore,
themeStore,
handleLogin
}
}
}
3. 状态持久化
// stores/persistence.js
import { watch } from 'vue'
import { useUserStore } from './userStore'
export function setupPersistence() {
const userStore = useUserStore()
// 监听状态变化并保存到 localStorage
watch(
() => userStore.user,
(newUser) => {
if (newUser) {
localStorage.setItem('user', JSON.stringify(newUser))
}
},
{ deep: true }
)
// 页面加载时恢复状态
const savedUser = localStorage.getItem('user')
if (savedUser) {
userStore.setUser(JSON.parse(savedUser))
}
}
性能优化技巧
1. 计算属性缓存优化
import { computed, ref } from 'vue'
// 避免在计算属性中进行复杂操作
const expensiveData = ref([])
const processedData = computed(() => {
// 简单的计算逻辑
return expensiveData.value.map(item => ({
...item,
processed: true
}))
})
// 对于更复杂的场景,可以使用缓存策略
const cachedResult = computed(() => {
// 只有当依赖变化时才重新计算
return expensiveOperation(expensiveData.value)
})
2. 组件渲染优化
// 使用 keep-alive 缓存组件
// App.vue
<template>
<keep-alive :include="cachedComponents">
<router-view />
</keep-alive>
</template>
<script setup>
import { ref } from 'vue'
const cachedComponents = ref(['ComponentA', 'ComponentB'])
</script>
// 使用 v-memo 避免不必要的渲染
// Component.vue
<template>
<div>
<!-- 只有当 data 变化时才重新渲染 -->
<div v-memo="[data]">
{{ expensiveRender(data) }}
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const data = ref({})
const expensiveRender = computed(() => {
// 复杂的渲染逻辑
return JSON.stringify(data.value)
})
</script>
3. 异步数据加载优化
// composables/useAsyncData.js
import { ref, computed } from 'vue'
export function useAsyncData(fetchFunction, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (...args) => {
loading.value = true
error.value = null
try {
const result = await fetchFunction(...args)
data.value = result
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
const refresh = () => execute()
// 防抖处理
const debouncedExecute = debounce(execute, options.debounceMs || 300)
return {
data,
loading,
error,
execute,
refresh,
debouncedExecute
}
}
// 防抖函数实现
function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
4. 虚拟滚动优化
// composables/useVirtualScroll.js
import { ref, computed, onMounted, onUnmounted } from 'vue'
export function useVirtualScroll(items, itemHeight) {
const containerRef = ref(null)
const scrollTop = ref(0)
const containerHeight = ref(0)
const visibleItems = computed(() => {
if (!containerRef.value || !items.value.length) return []
const startIndex = Math.floor(scrollTop.value / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight.value / itemHeight) + 1,
items.value.length
)
return items.value.slice(startIndex, endIndex)
})
const totalHeight = computed(() => items.value.length * itemHeight)
const handleScroll = () => {
if (containerRef.value) {
scrollTop.value = containerRef.value.scrollTop
}
}
onMounted(() => {
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight
containerRef.value.addEventListener('scroll', handleScroll)
}
})
onUnmounted(() => {
if (containerRef.value) {
containerRef.value.removeEventListener('scroll', handleScroll)
}
})
return {
containerRef,
visibleItems,
totalHeight,
scrollTop
}
}
实际项目应用案例
1. 电商购物车实现
// composables/useCart.js
import { ref, computed } from 'vue'
import { useUserStore } from '@/stores/userStore'
export function useCart() {
const cartItems = ref([])
const userStore = useUserStore()
const cartCount = computed(() => {
return cartItems.value.reduce((total, item) => total + item.quantity, 0)
})
const cartTotal = computed(() => {
return cartItems.value.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
})
const addToCart = (product) => {
const existingItem = cartItems.value.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity += 1
} else {
cartItems.value.push({
...product,
quantity: 1
})
}
// 同步到服务器
syncCartToServer()
}
const removeFromCart = (productId) => {
cartItems.value = cartItems.value.filter(item => item.id !== productId)
syncCartToServer()
}
const updateQuantity = (productId, quantity) => {
const item = cartItems.value.find(item => item.id === productId)
if (item) {
item.quantity = Math.max(0, quantity)
if (item.quantity === 0) {
removeFromCart(productId)
} else {
syncCartToServer()
}
}
}
const syncCartToServer = async () => {
if (!userStore.isLoggedIn) return
try {
await fetch('/api/cart/sync', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
items: cartItems.value
})
})
} catch (error) {
console.error('Failed to sync cart:', error)
}
}
return {
cartItems,
cartCount,
cartTotal,
addToCart,
removeFromCart,
updateQuantity
}
}
2. 数据表格组件
<!-- DataTable.vue -->
<template>
<div class="data-table">
<div class="table-header">
<input
v-model="searchQuery"
placeholder="搜索..."
class="search-input"
/>
<select v-model="sortBy" class="sort-select">
<option value="">排序</option>
<option v-for="field in columns" :key="field.key" :value="field.key">
{{ field.title }}
</option>
</select>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th
v-for="column in columns"
:key="column.key"
@click="handleSort(column.key)"
>
{{ column.title }}
<span v-if="sortBy === column.key && sortDirection === 'asc'">↑</span>
<span v-else-if="sortBy === column.key && sortDirection === 'desc'">↓</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in paginatedData" :key="row.id">
<td v-for="column in columns" :key="column.key">
<component
:is="column.component || 'span'"
:value="row[column.key]"
:data="row"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button @click="currentPage--" :disabled="currentPage === 1">
上一页
</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button @click="currentPage++" :disabled="currentPage === totalPages">
下一页
</button>
</div>
</div>
</template>
<script setup>
import {
ref,
computed,
watch,
defineProps,
defineEmits
} from 'vue'
const props = defineProps({
data: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
},
pageSize: {
type: Number,
default: 10
}
})
const emit = defineEmits(['sort', 'pageChange'])
const searchQuery = ref('')
const sortBy = ref('')
const sortDirection = ref('asc')
const currentPage = ref(1)
const filteredData = computed(() => {
if (!searchQuery.value) return props.data
const query = searchQuery.value.toLowerCase()
return props.data.filter(item => {
return Object.values(item).some(value =>
value.toString().toLowerCase().includes(query)
)
})
})
const sortedData = computed(() => {
if (!sortBy.value) return filteredData.value
return [...filteredData.value].sort((a, b) => {
const aValue = a[sortBy.value]
const bValue = b[sortBy.value]
if (aValue < bValue) {
return sortDirection.value === 'asc' ? -1 : 1
}
if (aValue > bValue) {
return sortDirection.value === 'asc' ? 1 : -1
}
return 0
})
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * props.pageSize
const end = start + props.pageSize
return sortedData.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(sortedData.value.length / props.pageSize)
})
const handleSort = (field) => {
if (sortBy.value === field) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = field
sortDirection.value = 'asc'
}
emit('sort', { field, direction: sortDirection.value })
}
// 监听分页变化
watch(currentPage, (newPage) => {
emit('pageChange', newPage)
})
// 监听数据变化
watch(() => props.data, () => {
currentPage.value = 1
})
</script>
<style scoped>
.data-table {
padding: 20px;
}
.table-header {
display: flex;
gap: 10px;
margin-bottom: 20px;
align-items: center;
}
.search-input, .sort-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
cursor: pointer;
user-select: none;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
总结与最佳实践
关键要点回顾
- 合理使用 Composition API:根据项目复杂度选择合适的 API 风格
- 组件通信策略:根据层级关系选择适当的通信方式
- 状态管理规范:建立统一的状态管理模式,避免状态混乱
- 性能优化意识:从数据处理、渲染优化等多个维度提升应用性能
最佳实践建议
- 模块化设计:将相关的逻辑封装成组合函数
- 类型安全:充分利用 TypeScript 提供的类型检查
- 错误处理:建立完善的错误处理机制
- 测试友好:编写可测试的组件和组合函数
- 文档完善:为复杂的组合函数提供清晰的文档说明
未来发展趋势
随着 Vue 生态系统的不断发展,Composition API 将继续演进。我们可以期待:
- 更强大的 TypeScript 支持
- 更丰富的组合函数生态系统
- 更好的性能优化工具和最佳实践
- 与现代前端开发工具的深度集成
通过本文的介绍和实践案例,相信读者已经对 Vue 3 Composition API 的使用有了深入的理解。在实际项目中,建议根据具体需求灵活运用这些技术,持续优化应用的性能和用户体验。
记住,技术的选择应该服务于业务目标,而不是为了炫技而使用新技术。合理地运用 Composition API,能够让我们构建出更加优雅、可维护和高性能的 Vue 应用程序。

评论 (0)