Vue 3 Composition API实战:组件状态管理与性能优化最佳实践

NarrowEve
NarrowEve 2026-02-03T08:17:05+08:00
0 0 0

引言

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 的核心优势

  1. 更好的逻辑复用:通过组合函数(composables)实现逻辑的复用
  2. 更灵活的代码组织:按功能而非选项来组织代码
  3. 更强的类型支持:与 TypeScript 集成更紧密
  4. 更好的性能优化:减少不必要的渲染和计算

基础 API 详解

refreactive

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' // 直接修改

computedwatch

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
    }
  }
}

性能优化最佳实践

组件渲染优化

使用 memokeep-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)

    0/2000