引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比传统的 Options API,Composition API 提供了更灵活、更强大的组件开发方式,特别是在处理复杂组件逻辑时表现尤为突出。本文将深入探讨 Vue 3 Composition API 的核心概念和使用技巧,并结合实际项目案例,分享组件状态管理、响应式数据处理和性能优化的实用经验与最佳实践。
Vue 3 Composition API 核心概念
什么是 Composition API
Composition API 是 Vue 3 中引入的一种新的组件开发方式,它允许开发者以函数的形式组织和复用逻辑代码。与传统的 Options API(基于选项的对象)不同,Composition API 将组件的逻辑按照功能模块进行拆分,使得代码更加清晰、可维护性更高。
Composition API 的核心优势
- 更好的逻辑复用:通过组合函数(composables)实现逻辑的复用
- 更灵活的代码组织:按功能而非选项来组织代码
- 更强的类型支持:与 TypeScript 集成更紧密
- 更好的性能优化:减少不必要的渲染和计算
基础 API 详解
ref 和 reactive
import { ref, reactive } from 'vue'
// 使用 ref 创建响应式数据
const count = ref(0)
const name = ref('Vue')
// 使用 reactive 创建响应式对象
const state = reactive({
user: {
name: 'John',
age: 25
},
items: []
})
// 访问和修改
console.log(count.value) // 0
count.value = 10
state.user.name = 'Jane' // 直接修改
computed 和 watch
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 监听器
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
// 深度监听
const user = reactive({ profile: { name: 'John' } })
watch(user, (newVal, oldVal) => {
console.log('user changed')
}, { deep: true })
组件状态管理实战
基础状态管理
在 Vue 3 中,我们可以使用 Composition API 来管理组件的复杂状态:
// components/UserProfile.vue
import { ref, reactive, computed } from 'vue'
export default {
name: 'UserProfile',
setup() {
// 响应式数据
const user = ref(null)
const loading = ref(false)
const error = ref(null)
// 状态对象
const formState = reactive({
name: '',
email: '',
phone: ''
})
// 计算属性
const isValidForm = computed(() => {
return formState.name && formState.email
})
// 方法
const fetchUser = async (userId) => {
loading.value = true
try {
const response = await fetch(`/api/users/${userId}`)
user.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const updateUser = async () => {
try {
const response = await fetch(`/api/users/${user.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formState)
})
user.value = await response.json()
} catch (err) {
error.value = err.message
}
}
return {
user,
loading,
error,
formState,
isValidForm,
fetchUser,
updateUser
}
}
}
复杂状态管理示例
对于更复杂的状态管理场景,我们可以创建专门的组合函数:
// composables/usePagination.js
import { ref, computed } from 'vue'
export function usePagination(totalItems, itemsPerPage = 10) {
const currentPage = ref(1)
const pageSize = ref(itemsPerPage)
const totalPages = computed(() => {
return Math.ceil(totalItems.value / pageSize.value)
})
const hasNextPage = computed(() => {
return currentPage.value < totalPages.value
})
const hasPrevPage = computed(() => {
return currentPage.value > 1
})
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++
}
}
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--
}
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
return {
currentPage,
pageSize,
totalPages,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage
}
}
// composables/useFilter.js
import { ref, computed } from 'vue'
export function useFilter(items, filters) {
const filterState = ref(filters || {})
const filteredItems = computed(() => {
return items.value.filter(item => {
return Object.entries(filterState.value).every(([key, value]) => {
if (!value) return true
return item[key].toLowerCase().includes(value.toLowerCase())
})
})
})
const setFilter = (key, value) => {
filterState.value[key] = value
}
const clearFilters = () => {
Object.keys(filterState.value).forEach(key => {
filterState.value[key] = ''
})
}
return {
filterState,
filteredItems,
setFilter,
clearFilters
}
}
在组件中使用组合函数
// components/ProductList.vue
import { ref, onMounted } from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useFilter } from '@/composables/useFilter'
export default {
name: 'ProductList',
setup() {
const products = ref([])
const loading = ref(false)
// 使用分页组合函数
const pagination = usePagination(ref(0), 12)
// 使用过滤组合函数
const filters = { name: '', category: '' }
const filter = useFilter(products, filters)
const fetchProducts = async () => {
loading.value = true
try {
const response = await fetch('/api/products')
const data = await response.json()
products.value = data
pagination.totalItems = data.length
} catch (error) {
console.error('Failed to fetch products:', error)
} finally {
loading.value = false
}
}
// 分页处理
const handlePageChange = (page) => {
pagination.goToPage(page)
// 这里可以添加分页加载逻辑
}
onMounted(() => {
fetchProducts()
})
return {
products: filter.filteredItems,
loading,
pagination,
filters: filter.filterState,
handlePageChange,
setFilter: filter.setFilter
}
}
}
响应式数据处理技巧
高级响应式模式
深度响应式与浅响应式
import { ref, reactive, shallowRef, toRaw } from 'vue'
// 深度响应式(默认行为)
const deepState = reactive({
user: {
profile: {
name: 'John'
}
}
})
// 浅响应式(只响应顶层属性变化)
const shallowState = shallowRef({
user: {
profile: {
name: 'John'
}
}
})
// 修改深层属性
deepState.user.profile.name = 'Jane' // 触发更新
shallowState.value.user.profile.name = 'Jane' // 不触发更新
// 获取原始对象
const rawObject = toRaw(shallowState.value)
响应式数据的性能优化
import { ref, computed, watch } from 'vue'
export default {
setup() {
const data = ref([])
// 使用计算属性缓存复杂计算结果
const expensiveComputation = computed(() => {
return data.value.reduce((sum, item) => {
// 复杂的计算逻辑
return sum + item.value * 2
}, 0)
})
// 监听特定数据变化
watch(data, (newData) => {
// 只在数据真正变化时执行
console.log('Data changed:', newData.length)
}, { deep: true })
// 节流监听器
const debouncedWatch = (source, callback, delay = 300) => {
let timeoutId
return watch(source, (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => callback(...args), delay)
})
}
return {
data,
expensiveComputation
}
}
}
自定义响应式逻辑
// 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
}
// 使用示例
export default {
setup() {
const theme = useLocalStorage('theme', 'light')
const userPreferences = useLocalStorage('userPrefs', {
notifications: true,
language: 'en'
})
return {
theme,
userPreferences
}
}
}
性能优化最佳实践
组件渲染优化
使用 memo 和 keep-alive
// components/OptimizedList.vue
import { ref, computed } from 'vue'
export default {
name: 'OptimizedList',
props: {
items: {
type: Array,
required: true
}
},
setup(props) {
// 使用计算属性缓存结果
const processedItems = computed(() => {
return props.items.map(item => ({
...item,
processedAt: new Date()
}))
})
// 避免不必要的重新渲染
const renderItem = (item, index) => {
// 只在必要时计算
if (!item.visible) return null
return h('div', {
key: item.id,
class: 'list-item'
}, [
h('span', item.name),
h('button', {
onClick: () => toggleItem(item.id)
}, 'Toggle')
])
}
return {
processedItems
}
}
}
虚拟滚动优化
// composables/useVirtualScroll.js
import { ref, computed, onMounted, onUnmounted } from 'vue'
export function useVirtualScroll(items, itemHeight = 50) {
const containerRef = ref(null)
const scrollTop = ref(0)
const containerHeight = ref(0)
const visibleItems = computed(() => {
if (!containerRef.value) return []
const startIndex = Math.floor(scrollTop.value / itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight.value / itemHeight),
items.value.length
)
return items.value.slice(startIndex, endIndex)
})
const totalHeight = computed(() => {
return 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
}
}
数据加载优化
请求缓存和防抖
// composables/useApi.js
import { ref, computed } from 'vue'
class ApiCache {
constructor() {
this.cache = new Map()
this.ttl = 5 * 60 * 1000 // 5分钟
}
get(key) {
const cached = this.cache.get(key)
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data
}
return null
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
clear() {
this.cache.clear()
}
}
const apiCache = new ApiCache()
export function useApi() {
const loading = ref(false)
const error = ref(null)
const fetchWithCache = async (url, options = {}) => {
const cacheKey = `${url}_${JSON.stringify(options)}`
// 检查缓存
const cachedData = apiCache.get(cacheKey)
if (cachedData) {
return cachedData
}
loading.value = true
error.value = null
try {
const response = await fetch(url, options)
const data = await response.json()
// 缓存数据
apiCache.set(cacheKey, data)
return data
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
// 防抖函数
const debounce = (func, wait) => {
let timeoutId
return (...args) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), wait)
}
}
return {
loading,
error,
fetchWithCache,
debounce
}
}
组件级优化策略
使用 defineAsyncComponent 进行懒加载
import { defineAsyncComponent } from 'vue'
export default {
components: {
AsyncChart: defineAsyncComponent(() =>
import('@/components/Chart.vue')
),
AsyncEditor: defineAsyncComponent(() =>
import('@/components/Editor.vue')
)
}
}
条件渲染优化
// components/ConditionalRender.vue
import { ref, computed } from 'vue'
export default {
setup() {
const showDetails = ref(false)
const expandedSections = ref(new Set())
// 使用计算属性优化条件渲染
const shouldShowContent = computed(() => {
return showDetails.value || expandedSections.value.size > 0
})
const toggleSection = (sectionId) => {
const sections = new Set(expandedSections.value)
if (sections.has(sectionId)) {
sections.delete(sectionId)
} else {
sections.add(sectionId)
}
expandedSections.value = sections
}
return {
showDetails,
shouldShowContent,
toggleSection
}
}
}
实际项目案例分析
电商商品详情页
<template>
<div class="product-detail">
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<h1>{{ product.name }}</h1>
<div class="product-images">
<img :src="mainImage" :alt="product.name" />
</div>
<div class="product-info">
<p class="price">{{ formatPrice(product.price) }}</p>
<p class="description">{{ product.description }}</p>
<div class="actions">
<button @click="addToCart">Add to Cart</button>
<button @click="toggleWishlist" :class="{ active: isInWishlist }">
{{ isInWishlist ? 'Remove from Wishlist' : 'Add to Wishlist' }}
</button>
</div>
</div>
<!-- 评论区 -->
<div class="reviews">
<h3>Reviews ({{ reviews.length }})</h3>
<div v-for="review in paginatedReviews" :key="review.id" class="review-item">
<p>{{ review.content }}</p>
<p class="rating">Rating: {{ review.rating }}/5</p>
</div>
<button @click="loadMoreReviews" v-if="hasMoreReviews">Load More</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { usePagination } from '@/composables/usePagination'
export default {
name: 'ProductDetail',
props: {
productId: {
type: String,
required: true
}
},
setup(props) {
const product = ref(null)
const reviews = ref([])
const loading = ref(false)
const error = ref(null)
// 分页配置
const pagination = usePagination(ref(0), 5)
const mainImage = computed(() => {
return product.value?.images?.[0] || ''
})
const isInWishlist = computed(() => {
return localStorage.getItem(`wishlist_${props.productId}`) === 'true'
})
const paginatedReviews = computed(() => {
return reviews.value.slice(0, pagination.currentPage * 5)
})
const hasMoreReviews = computed(() => {
return reviews.value.length > paginatedReviews.value.length
})
const formatPrice = (price) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(price)
}
const fetchProduct = async () => {
loading.value = true
try {
const [productRes, reviewsRes] = await Promise.all([
fetch(`/api/products/${props.productId}`),
fetch(`/api/products/${props.productId}/reviews`)
])
product.value = await productRes.json()
reviews.value = await reviewsRes.json()
pagination.totalItems = reviews.value.length
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const addToCart = () => {
// 添加到购物车逻辑
console.log('Added to cart:', product.value.name)
}
const toggleWishlist = () => {
const key = `wishlist_${props.productId}`
const currentValue = localStorage.getItem(key)
const newValue = currentValue === 'true' ? 'false' : 'true'
localStorage.setItem(key, newValue)
// 可以添加通知或其他逻辑
console.log('Wishlist updated')
}
const loadMoreReviews = () => {
pagination.nextPage()
}
onMounted(() => {
fetchProduct()
})
return {
product,
reviews,
loading,
error,
mainImage,
isInWishlist,
paginatedReviews,
hasMoreReviews,
formatPrice,
addToCart,
toggleWishlist,
loadMoreReviews
}
}
}
</script>
<style scoped>
.product-detail {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.product-images img {
max-width: 100%;
height: auto;
}
.price {
font-size: 2em;
color: #e74c3c;
font-weight: bold;
}
.actions {
margin: 20px 0;
}
.actions button {
margin-right: 10px;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.actions button.active {
background-color: #3498db;
color: white;
}
.reviews {
margin-top: 40px;
}
.review-item {
padding: 15px;
border-bottom: 1px solid #eee;
}
.rating {
color: #f39c12;
}
</style>
数据可视化仪表板
<template>
<div class="dashboard">
<div class="dashboard-header">
<h1>Analytics Dashboard</h1>
<div class="time-filters">
<button
v-for="filter in timeFilters"
:key="filter.value"
:class="{ active: selectedTimeFilter === filter.value }"
@click="selectTimeFilter(filter.value)"
>
{{ filter.label }}
</button>
</div>
</div>
<div class="dashboard-charts">
<div class="chart-container">
<h2>Sales Overview</h2>
<LineChart :data="salesData" :loading="loading.sales" />
</div>
<div class="chart-container">
<h2>User Activity</h2>
<BarChart :data="activityData" :loading="loading.activity" />
</div>
<div class="chart-container">
<h2>Conversion Rate</h2>
<PieChart :data="conversionData" :loading="loading.conversion" />
</div>
</div>
<div class="dashboard-summary">
<div class="summary-card" v-for="card in summaryCards" :key="card.title">
<h3>{{ card.title }}</h3>
<p class="value">{{ card.value }}</p>
<p class="change" :class="{ positive: card.change > 0, negative: card.change < 0 }">
{{ card.change > 0 ? '+' : '' }}{{ card.change }}%
</p>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue'
import LineChart from '@/components/charts/LineChart.vue'
import BarChart from '@/components/charts/BarChart.vue'
import PieChart from '@/components/charts/PieChart.vue'
export default {
name: 'AnalyticsDashboard',
components: {
LineChart,
BarChart,
PieChart
},
setup() {
const selectedTimeFilter = ref('7d')
const loading = ref({
sales: false,
activity: false,
conversion: false
})
const salesData = ref([])
const activityData = ref([])
const conversionData = ref([])
const timeFilters = [
{ label: 'Last 7 Days', value: '7d' },
{ label: 'Last 30 Days', value: '30d' },
{ label: 'Last 90 Days', value: '90d' }
]
const summaryCards = computed(() => [
{
title: 'Total Revenue',
value: '$45,231.89',
change: 12.5
},
{
title: 'Subscriptions',
value: '+2350',
change: 18.2
},
{
title: 'Sales',
value: '+12,234',
change: -2.3
}
])
const fetchDashboardData = async () => {
loading.value.sales = true
loading.value.activity = true
loading.value.conversion = true
try {
const [salesRes, activityRes, conversionRes] = await Promise.all([
fetch(`/api/analytics/sales?period=${selectedTimeFilter.value}`),
fetch(`/api/analytics/activity?period=${selectedTimeFilter.value}`),
fetch(`/api/analytics/conversion?period=${selectedTimeFilter.value}`)
])
salesData.value = await salesRes.json()
activityData.value = await activityRes.json()
conversionData.value = await conversionRes.json()
} catch (error) {
console.error('Failed to fetch dashboard data:', error)
} finally {
loading.value.sales = false
loading.value.activity = false
loading.value.conversion = false
}
}
const selectTimeFilter = (filterValue) => {
selectedTimeFilter.value = filterValue
}
// 监听时间过滤器变化
watch(selectedTimeFilter, () => {
fetchDashboardData()
})
onMounted(() => {
fetchDashboardData()
})
return {
selectedTimeFilter,
loading,
salesData,
activityData,
conversionData,
timeFilters,
summaryCards,
selectTimeFilter
}
}
}
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.time-filters button {
margin-right: 10px;
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 4px;
}
.time-filters button.active {
background-color: #3498db;
color: white;
border-color: #3498db;
}
.dashboard-charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dashboard-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.value {
font-size: 2em;
font-weight: bold;
margin: 10px 0;
}
.change {
font-weight: bold;
}
.change.positive {
color: #27ae60;
}
.change.negative {
color: #e74c3c;
}
</style>
性能监控与调试
Vue DevTools 集成
// utils/performance.js
import { ref, computed } from 'vue'
export function usePerformanceMonitor() {
const performanceData = ref({
renderTime: 0,
updateCount: 0,
memoryUsage: 0
})
const startTimer = () => {
return performance.now()
}
const endTimer = (startTime) => {
return performance.now() - startTime
}
const trackRenderTime = (callback) =>
评论 (0)