Vue 3 Composition API实战:组件通信、状态管理与性能优化完整指南

LowGhost
LowGhost 2026-01-26T09:12:20+08:00
0 0 1

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。作为 Vue 3 的核心特性之一,Composition API 不仅提供了更灵活的代码组织方式,还为组件通信、状态管理和性能优化带来了全新的可能性。本文将深入探讨 Composition API 的核心特性,并结合实际项目经验,分享如何在真实场景中应用这些技术来构建高性能的 Vue 应用。

什么是 Composition API

核心概念

Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式,它允许我们使用函数来组织和复用组件逻辑。与传统的 Options API 相比,Composition API 更加灵活,能够更好地处理复杂的组件逻辑,特别是在需要在多个组件间共享逻辑时。

主要优势

  1. 更好的逻辑复用:通过组合函数实现逻辑的复用
  2. 更清晰的代码组织:将相关的逻辑组织在一起
  3. 更强的类型支持:与 TypeScript 集成更好
  4. 更好的开发体验:更适合大型项目的维护

组件通信实战

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>

总结与最佳实践

关键要点回顾

  1. 合理使用 Composition API:根据项目复杂度选择合适的 API 风格
  2. 组件通信策略:根据层级关系选择适当的通信方式
  3. 状态管理规范:建立统一的状态管理模式,避免状态混乱
  4. 性能优化意识:从数据处理、渲染优化等多个维度提升应用性能

最佳实践建议

  1. 模块化设计:将相关的逻辑封装成组合函数
  2. 类型安全:充分利用 TypeScript 提供的类型检查
  3. 错误处理:建立完善的错误处理机制
  4. 测试友好:编写可测试的组件和组合函数
  5. 文档完善:为复杂的组合函数提供清晰的文档说明

未来发展趋势

随着 Vue 生态系统的不断发展,Composition API 将继续演进。我们可以期待:

  • 更强大的 TypeScript 支持
  • 更丰富的组合函数生态系统
  • 更好的性能优化工具和最佳实践
  • 与现代前端开发工具的深度集成

通过本文的介绍和实践案例,相信读者已经对 Vue 3 Composition API 的使用有了深入的理解。在实际项目中,建议根据具体需求灵活运用这些技术,持续优化应用的性能和用户体验。

记住,技术的选择应该服务于业务目标,而不是为了炫技而使用新技术。合理地运用 Composition API,能够让我们构建出更加优雅、可维护和高性能的 Vue 应用程序。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000