Vue 3 Composition API实战:组件复用与状态管理高级技巧

ThickBody
ThickBody 2026-03-04T23:06:11+08:00
0 0 0

引言

Vue 3的发布带来了全新的Composition API,这一创新性的API设计彻底改变了我们构建Vue应用的方式。相比于Vue 2的选项式API,Composition API提供了更灵活、更强大的组件逻辑组织方式,特别是在处理复杂组件和状态管理方面展现出了卓越的能力。

本文将深入探讨Vue 3 Composition API的核心概念,详细解析setup函数的使用、响应式API的高级特性,以及如何通过组合式逻辑复用来实现组件的高效复用。通过构建实际的复杂组件和状态管理方案,我们将展示Composition API在现代前端开发中的强大能力。

Vue 3 Composition API核心概念

什么是Composition API

Composition API是Vue 3引入的一种新的组件逻辑组织方式。它允许我们将组件的逻辑按照功能进行分组,而不是按照选项类型进行组织。这种设计模式使得代码更加灵活,便于维护和复用。

与Vue 2的选项式API相比,Composition API的主要优势包括:

  1. 更好的逻辑复用:通过组合函数实现逻辑复用,避免了混入(mixins)的命名冲突问题
  2. 更灵活的代码组织:可以按照功能逻辑而非选项类型来组织代码
  3. 更强的类型支持:在TypeScript环境中提供更好的类型推断
  4. 更清晰的代码结构:逻辑更加集中,便于理解和维护

setup函数详解

setup函数是Composition API的核心入口。它在组件实例创建之前执行,接收props和context作为参数。

// 基本setup函数用法
export default {
  setup(props, context) {
    // 组件逻辑
    return {
      // 需要暴露给模板的属性和方法
    }
  }
}

setup函数的返回值可以是一个对象,该对象的属性会被暴露给模板使用。同时,setup函数内部可以访问组件的props和上下文信息。

import { ref, reactive } from 'vue'

export default {
  props: {
    title: String,
    count: Number
  },
  setup(props, context) {
    // 访问props
    console.log(props.title)
    
    // 使用响应式数据
    const counter = ref(0)
    const state = reactive({
      name: 'Vue',
      version: '3.0'
    })
    
    // 定义方法
    const increment = () => {
      counter.value++
    }
    
    // 返回给模板使用的数据和方法
    return {
      counter,
      state,
      increment
    }
  }
}

响式API高级特性

ref与reactive的区别与使用

在Composition API中,ref和reactive是两个核心的响应式API,它们各有不同的使用场景。

import { ref, reactive, toRefs, toRaw } from 'vue'

// ref用于基本数据类型
const count = ref(0)
const message = ref('Hello')

// reactive用于对象类型
const state = reactive({
  user: {
    name: 'John',
    age: 30
  },
  items: []
})

// 访问ref值需要使用.value
console.log(count.value) // 0
count.value = 10

// 访问reactive对象的属性无需.value
console.log(state.user.name) // John

响应式数据的深入理解

import { ref, reactive, watch, watchEffect } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const user = reactive({
      name: 'Alice',
      age: 25
    })
    
    // watch监听单个响应式数据
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    // watch监听对象属性
    watch(() => user.name, (newVal, oldVal) => {
      console.log(`name changed from ${oldVal} to ${newVal}`)
    })
    
    // watchEffect自动追踪依赖
    watchEffect(() => {
      console.log(`User: ${user.name}, Age: ${user.age}`)
    })
    
    // 深度监听
    const deepState = reactive({
      nested: {
        data: {
          value: 1
        }
      }
    })
    
    watch(deepState, (newVal) => {
      console.log('Deep state changed:', newVal)
    }, { deep: true })
    
    return {
      count,
      user,
      deepState
    }
  }
}

computed计算属性的高级用法

import { ref, computed } from 'vue'

export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')
    const age = ref(30)
    
    // 基本计算属性
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`
    })
    
    // 带getter和setter的计算属性
    const displayName = computed({
      get: () => {
        return `${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中实现逻辑复用的核心机制。通过创建可复用的组合函数,我们可以将通用的逻辑封装起来,供多个组件使用。

// 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 doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    doubleCount
  }
}

// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const value = ref(defaultValue)
  
  // 从localStorage初始化
  const savedValue = localStorage.getItem(key)
  if (savedValue) {
    value.value = JSON.parse(savedValue)
  }
  
  // 监听变化并保存到localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

// 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)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // 自动执行fetch
  watch(() => url, fetchData, { immediate: true })
  
  return {
    data,
    loading,
    error,
    refetch: fetchData
  }
}

实际应用示例:购物车组件

<!-- Cart.vue -->
<template>
  <div class="cart">
    <h2>购物车</h2>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <div v-for="item in cartItems" :key="item.id" class="cart-item">
        <span>{{ item.name }}</span>
        <span>数量: {{ item.quantity }}</span>
        <span>价格: ¥{{ item.price }}</span>
        <button @click="removeItem(item.id)">删除</button>
      </div>
      <div class="cart-total">
        <p>总计: ¥{{ total }}</p>
        <button @click="checkout">结算</button>
      </div>
    </div>
  </div>
</template>

<script>
import { computed } from 'vue'
import { useCart } from '@/composables/useCart'

export default {
  name: 'Cart',
  setup() {
    const { 
      cartItems, 
      loading, 
      error, 
      removeItem, 
      checkout 
    } = useCart()
    
    const total = computed(() => {
      return cartItems.value.reduce((sum, item) => {
        return sum + (item.price * item.quantity)
      }, 0)
    })
    
    return {
      cartItems,
      loading,
      error,
      removeItem,
      checkout,
      total
    }
  }
}
</script>

<style scoped>
.cart {
  padding: 20px;
}

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #eee;
  margin-bottom: 10px;
}

.cart-total {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #eee;
}
</style>

组合函数的高级用法

// composables/useForm.js
import { ref, reactive } from 'vue'

export function useForm(initialData = {}) {
  const formData = reactive({ ...initialData })
  const errors = ref({})
  const isSubmitting = ref(false)
  
  const validate = (rules) => {
    const newErrors = {}
    Object.keys(rules).forEach(field => {
      const rule = rules[field]
      const value = formData[field]
      
      if (rule.required && !value) {
        newErrors[field] = `${field} 是必填项`
      }
      
      if (rule.minLength && value && value.length < rule.minLength) {
        newErrors[field] = `${field} 长度不能少于${rule.minLength}位`
      }
      
      if (rule.pattern && value && !rule.pattern.test(value)) {
        newErrors[field] = `${field} 格式不正确`
      }
    })
    
    errors.value = newErrors
    return Object.keys(newErrors).length === 0
  }
  
  const submit = async (submitFn) => {
    if (!validate()) return
    
    isSubmitting.value = true
    try {
      const result = await submitFn(formData)
      return result
    } catch (error) {
      console.error('提交失败:', error)
      throw error
    } finally {
      isSubmitting.value = false
    }
  }
  
  const reset = () => {
    Object.keys(formData).forEach(key => {
      formData[key] = ''
    })
    errors.value = {}
  }
  
  return {
    formData,
    errors,
    isSubmitting,
    validate,
    submit,
    reset
  }
}

// composables/usePagination.js
import { ref, computed } from 'vue'

export function usePagination(data, pageSize = 10) {
  const currentPage = ref(1)
  const _pageSize = ref(pageSize)
  
  const totalPages = computed(() => {
    return Math.ceil(data.value.length / _pageSize.value)
  })
  
  const paginatedData = computed(() => {
    const start = (currentPage.value - 1) * _pageSize.value
    const end = start + _pageSize.value
    return data.value.slice(start, end)
  })
  
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  const nextPage = () => {
    if (currentPage.value < totalPages.value) {
      currentPage.value++
    }
  }
  
  const prevPage = () => {
    if (currentPage.value > 1) {
      currentPage.value--
    }
  }
  
  const setPageSize = (size) => {
    _pageSize.value = size
    currentPage.value = 1
  }
  
  return {
    currentPage,
    totalPages,
    paginatedData,
    goToPage,
    nextPage,
    prevPage,
    setPageSize
  }
}

状态管理高级技巧

基于Composition API的状态管理

// stores/useGlobalStore.js
import { ref, readonly } from 'vue'

// 全局状态管理
const user = ref(null)
const theme = ref('light')
const notifications = ref([])

// 状态操作方法
const setUser = (userData) => {
  user.value = userData
}

const setTheme = (newTheme) => {
  theme.value = newTheme
}

const addNotification = (notification) => {
  notifications.value.push({
    id: Date.now(),
    ...notification,
    timestamp: new Date()
  })
}

const removeNotification = (id) => {
  notifications.value = notifications.value.filter(n => n.id !== id)
}

const clearNotifications = () => {
  notifications.value = []
}

// 提供只读状态访问
const readonlyUser = readonly(user)
const readonlyTheme = readonly(theme)

export function useGlobalStore() {
  return {
    // 状态
    user: readonlyUser,
    theme: readonlyTheme,
    notifications: readonly(notifications),
    
    // 方法
    setUser,
    setTheme,
    addNotification,
    removeNotification,
    clearNotifications
  }
}

复杂状态管理示例:用户管理面板

<!-- UserManagement.vue -->
<template>
  <div class="user-management">
    <div class="user-header">
      <h2>用户管理</h2>
      <button @click="openCreateModal">添加用户</button>
    </div>
    
    <div class="filters">
      <input v-model="searchTerm" placeholder="搜索用户..." />
      <select v-model="filterRole">
        <option value="">所有角色</option>
        <option value="admin">管理员</option>
        <option value="user">普通用户</option>
      </select>
    </div>
    
    <div class="users-table">
      <table>
        <thead>
          <tr>
            <th>用户名</th>
            <th>邮箱</th>
            <th>角色</th>
            <th>状态</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="user in filteredUsers" :key="user.id">
            <td>{{ user.username }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.role }}</td>
            <td>
              <span :class="user.active ? 'status-active' : 'status-inactive'">
                {{ user.active ? '活跃' : '非活跃' }}
              </span>
            </td>
            <td>
              <button @click="editUser(user)">编辑</button>
              <button @click="toggleUserStatus(user)">切换状态</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    
    <div class="pagination">
      <button @click="prevPage" :disabled="currentPage === 1">上一页</button>
      <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
      <button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
    </div>
    
    <!-- 模态框 -->
    <UserModal 
      v-if="showModal" 
      :user="editingUser"
      :is-editing="isEditing"
      @save="saveUser"
      @close="closeModal"
    />
  </div>
</template>

<script>
import { 
  ref, 
  computed, 
  watch 
} from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useGlobalStore } from '@/stores/useGlobalStore'
import UserModal from './UserModal.vue'

export default {
  name: 'UserManagement',
  components: {
    UserModal
  },
  setup() {
    const { user: currentUser } = useGlobalStore()
    const users = ref([])
    const searchTerm = ref('')
    const filterRole = ref('')
    const showModal = ref(false)
    const isEditing = ref(false)
    const editingUser = ref(null)
    
    // 模拟数据加载
    const loadUsers = async () => {
      // 这里可以是API调用
      users.value = [
        { id: 1, username: 'admin', email: 'admin@example.com', role: 'admin', active: true },
        { id: 2, username: 'user1', email: 'user1@example.com', role: 'user', active: true },
        { id: 3, username: 'user2', email: 'user2@example.com', role: 'user', active: false }
      ]
    }
    
    // 加载用户数据
    loadUsers()
    
    // 分页逻辑
    const {
      currentPage,
      totalPages,
      paginatedData: paginatedUsers,
      nextPage,
      prevPage
    } = usePagination(users, 5)
    
    // 过滤用户
    const filteredUsers = computed(() => {
      let result = users.value
      
      if (searchTerm.value) {
        const term = searchTerm.value.toLowerCase()
        result = result.filter(user => 
          user.username.toLowerCase().includes(term) ||
          user.email.toLowerCase().includes(term)
        )
      }
      
      if (filterRole.value) {
        result = result.filter(user => user.role === filterRole.value)
      }
      
      return result
    })
    
    // 编辑用户
    const editUser = (user) => {
      editingUser.value = { ...user }
      isEditing.value = true
      showModal.value = true
    }
    
    // 打开创建用户模态框
    const openCreateModal = () => {
      editingUser.value = { username: '', email: '', role: 'user', active: true }
      isEditing.value = false
      showModal.value = true
    }
    
    // 保存用户
    const saveUser = async (userData) => {
      if (isEditing.value) {
        // 更新用户
        const index = users.value.findIndex(u => u.id === userData.id)
        if (index !== -1) {
          users.value[index] = userData
        }
      } else {
        // 创建用户
        const newUser = {
          ...userData,
          id: Date.now()
        }
        users.value.push(newUser)
      }
      closeModal()
    }
    
    // 关闭模态框
    const closeModal = () => {
      showModal.value = false
      editingUser.value = null
    }
    
    // 切换用户状态
    const toggleUserStatus = (user) => {
      const index = users.value.findIndex(u => u.id === user.id)
      if (index !== -1) {
        users.value[index].active = !users.value[index].active
      }
    }
    
    return {
      // 数据
      searchTerm,
      filterRole,
      users,
      filteredUsers,
      currentPage,
      totalPages,
      showModal,
      isEditing,
      editingUser,
      
      // 方法
      nextPage,
      prevPage,
      editUser,
      openCreateModal,
      saveUser,
      closeModal,
      toggleUserStatus
    }
  }
}
</script>

<style scoped>
.user-management {
  padding: 20px;
}

.user-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.users-table {
  margin-bottom: 20px;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
}

.status-active {
  color: green;
  font-weight: bold;
}

.status-inactive {
  color: red;
  font-weight: bold;
}
</style>

性能优化与最佳实践

响应式数据的优化

import { ref, reactive, computed, watch, watchEffect } from 'vue'

export default {
  setup() {
    // 1. 合理使用ref和reactive
    const count = ref(0) // 基本类型用ref
    const state = reactive({}) // 对象类型用reactive
    
    // 2. 使用computed优化计算属性
    const expensiveComputation = computed(() => {
      // 复杂计算逻辑
      return someExpensiveOperation()
    })
    
    // 3. 合理使用watch
    // 避免不必要的监听
    const watchOptions = {
      immediate: false, // 不立即执行
      deep: false,      // 不深度监听
      flush: 'post'     // 异步执行
    }
    
    watch(count, (newVal, oldVal) => {
      // 监听逻辑
    }, watchOptions)
    
    // 4. 使用watchEffect自动追踪依赖
    watchEffect(() => {
      // 自动追踪依赖,无需手动指定
      console.log(count.value)
    })
    
    return {
      count,
      expensiveComputation
    }
  }
}

组件性能优化

<!-- OptimizedComponent.vue -->
<template>
  <div class="optimized-component">
    <div v-for="item in optimizedList" :key="item.id" class="item">
      {{ item.name }}
    </div>
  </div>
</template>

<script>
import { 
  ref, 
  computed, 
  defineAsyncComponent 
} from 'vue'

export default {
  name: 'OptimizedComponent',
  setup() {
    const items = ref([])
    
    // 使用计算属性缓存结果
    const optimizedList = computed(() => {
      return items.value.filter(item => item.visible)
    })
    
    // 异步组件加载
    const AsyncComponent = defineAsyncComponent(() => 
      import('./AsyncComponent.vue')
    )
    
    // 防抖处理
    const debounce = (func, delay) => {
      let timeoutId
      return (...args) => {
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => func.apply(this, args), delay)
      }
    }
    
    const debouncedSearch = debounce((query) => {
      // 搜索逻辑
    }, 300)
    
    return {
      items,
      optimizedList,
      AsyncComponent,
      debouncedSearch
    }
  }
}
</script>

实际项目应用案例

电商商品列表组件

<!-- ProductList.vue -->
<template>
  <div class="product-list">
    <div class="filters">
      <select v-model="selectedCategory">
        <option value="">所有分类</option>
        <option v-for="category in categories" :key="category.id" :value="category.id">
          {{ category.name }}
        </option>
      </select>
      
      <input v-model="searchQuery" placeholder="搜索商品..." />
      
      <div class="price-filters">
        <input v-model="minPrice" type="number" placeholder="最低价格" />
        <input v-model="maxPrice" type="number" placeholder="最高价格" />
      </div>
    </div>
    
    <div class="loading" v-if="loading">加载中...</div>
    
    <div class="products-grid" v-else-if="products.length">
      <ProductCard 
        v-for="product in paginatedProducts" 
        :key="product.id"
        :product="product"
        @add-to-cart="handleAddToCart"
      />
    </div>
    
    <div class="no-products" v-else-if="!loading && products.length === 0">
      暂无商品
    </div>
    
    <div class="pagination" v-if="totalPages > 1">
      <button 
        v-for="page in paginationPages" 
        :key="page" 
        :class="{ active: currentPage === page }"
        @click="goToPage(page)"
      >
        {{ page }}
      </button>
    </div>
  </div>
</template>

<script>
import { 
  ref, 
  computed, 
  watch, 
  onMounted 
} from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useCart } from '@/composables/useCart'
import ProductCard from './ProductCard.vue'

export default {
  name: 'ProductList',
  components: {
    ProductCard
  },
  setup() {
    const products = ref([])
    const loading = ref(false)
    const searchQuery = ref('')
    const selectedCategory = ref('')
    const minPrice = ref('')
    const maxPrice = ref('')
    
    const categories = ref([
      { id: 'electronics', name: '电子产品' },
      { id: 'clothing', name: '服装' },
      { id: 'books', name: '图书' }
    ])
    
    // 模拟API调用
    const fetchProducts = async () => {
      loading.value = true
      try {
        // 模拟API延迟
        await new Promise(resolve => setTimeout(resolve, 1000))
        
        // 模拟产品数据
        products.value = [
          { id: 1, name: 'iPhone 14', price: 5999, category: 'electronics', image: '/images/iphone.jpg' },
          { id: 2, name: 'MacBook Pro', price: 12999, category: 'electronics', image: '/images/macbook.jpg' },
          { id: 3, name: 'T恤衫', price: 99, category: 'clothing', image: '/images/tshirt.jpg' },
          { id: 4, name: 'JavaScript高级程序设计', price: 89, category: 'books', image: '/images/js-book.jpg' }
        ]
      } catch (error) {
        console.error('获取产品失败:', error)
      } finally {
        loading.value = false
      }
    }
    
    // 过滤产品
    const filteredProducts = computed(() => {
      let result = products.value
      
      if (searchQuery.value) {
        const term = searchQuery.value.toLowerCase()
        result = result.filter(product => 
          product.name.toLowerCase().includes(term)
        )
      }
      
      if (selectedCategory.value) {
        result = result.filter(product => 
          product.category === selectedCategory.value
        )
      }
      
      if (minPrice.value) {
        result = result.filter(product => 
          product.price >= parseFloat(minPrice.value)
        )
      }
      
      if (maxPrice.value) {
        result = result.filter(product => 
          product.price <= parseFloat(maxPrice.value)
        )
      }
      
      return result
    })
    
    // 分页处理
    const {
      currentPage,
      totalPages,
      paginatedData: paginatedProducts,
      goToPage
    } = usePagination(filteredProducts, 6)
    
    // 计算分页按钮
    const paginationPages = computed(() => {
      const pages = []
      const maxVisible = 5
      const start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000