Vue 3 Composition API实战:组件状态管理与响应式编程深度应用

CalmSilver
CalmSilver 2026-02-04T20:08:04+08:00
0 0 1

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。这一新特性为开发者提供了更加灵活和强大的组件开发方式,特别是在处理复杂组件逻辑时表现出色。本文将深入探讨 Vue 3 Composition API 的核心特性,包括 setup 函数、响应式 API 以及组合式逻辑复用等,并通过实际项目案例展示如何构建高效、可维护的现代化前端应用。

Vue 3 Composition API 核心概念

什么是 Composition API?

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

Setup 函数详解

setup 是 Composition API 的入口函数,它在组件实例创建之前执行,用于定义响应式数据、计算属性、方法等。setup 函数接收两个参数:props 和 context。

import { ref, reactive } from 'vue'

export default {
  props: {
    title: String
  },
  setup(props, context) {
    // 在这里定义响应式数据和逻辑
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    return {
      count,
      user
    }
  }
}

响应式 API 深度解析

Ref API 的使用

ref 是最基础的响应式 API,用于创建一个响应式的引用。它可以包装任何类型的值,并提供 .value 属性来访问和修改值。

import { ref } from 'vue'

// 创建基本类型响应式数据
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

// 创建对象响应式数据
const obj = ref({ name: 'Vue', version: '3.0' })
console.log(obj.value.name) // Vue
obj.value.name = 'React'
console.log(obj.value.name) // React

Reactive API 的应用

reactive 用于创建响应式的对象,与 ref 不同的是,它直接返回一个响应式对象,不需要通过 .value 访问。

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'John',
    age: 30
  }
})

// 修改数据会自动触发更新
state.count = 1
state.user.name = 'Jane'

Computed 计算属性

computed 用于创建计算属性,它会根据依赖的响应式数据自动计算并缓存结果。

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 基础用法
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 带有 getter 和 setter 的计算属性
const reversedName = computed({
  get: () => {
    return firstName.value.split('').reverse().join('')
  },
  set: (value) => {
    const names = value.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

组件状态管理实战

简单计数器组件示例

让我们从一个简单的计数器组件开始,展示如何使用 Composition API 管理组件状态:

<template>
  <div class="counter">
    <h2>{{ title }}</h2>
    <p>当前计数: {{ count }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
  </div>
</template>

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

export default {
  name: 'Counter',
  props: {
    title: {
      type: String,
      default: '计数器'
    }
  },
  setup(props) {
    // 响应式状态
    const count = ref(0)
    
    // 计算属性
    const isPositive = computed(() => count.value >= 0)
    
    // 方法
    const increment = () => {
      count.value++
    }
    
    const decrement = () => {
      count.value--
    }
    
    const reset = () => {
      count.value = 0
    }
    
    // 返回给模板使用的数据和方法
    return {
      count,
      isPositive,
      increment,
      decrement,
      reset
    }
  }
}
</script>

<style scoped>
.counter {
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin: 10px;
}

button {
  margin: 5px;
  padding: 8px 16px;
  cursor: pointer;
}
</style>

复杂表单组件状态管理

在实际项目中,我们经常需要处理复杂的表单逻辑。下面是一个购物车商品编辑组件的示例:

<template>
  <div class="product-form">
    <h3>{{ title }}</h3>
    
    <!-- 基本信息 -->
    <div class="form-group">
      <label>商品名称:</label>
      <input v-model="formData.name" type="text" />
    </div>
    
    <div class="form-group">
      <label>价格:</label>
      <input v-model.number="formData.price" type="number" />
    </div>
    
    <div class="form-group">
      <label>库存数量:</label>
      <input v-model.number="formData.stock" type="number" />
    </div>
    
    <!-- 分类选择 -->
    <div class="form-group">
      <label>商品分类:</label>
      <select v-model="formData.category">
        <option value="">请选择分类</option>
        <option value="electronics">电子产品</option>
        <option value="clothing">服装</option>
        <option value="books">图书</option>
      </select>
    </div>
    
    <!-- 标签选择 -->
    <div class="form-group">
      <label>标签:</label>
      <div class="tag-container">
        <span 
          v-for="tag in availableTags" 
          :key="tag"
          :class="{ active: formData.tags.includes(tag) }"
          @click="toggleTag(tag)"
          class="tag"
        >
          {{ tag }}
        </span>
      </div>
    </div>
    
    <!-- 保存按钮 -->
    <button @click="save" :disabled="!isFormValid">保存</button>
    <button @click="reset">重置</button>
    
    <!-- 表单验证信息 -->
    <div v-if="validationErrors.length" class="errors">
      <ul>
        <li v-for="error in validationErrors" :key="error">{{ error }}</li>
      </ul>
    </div>
  </div>
</template>

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

export default {
  name: 'ProductForm',
  props: {
    title: {
      type: String,
      default: '商品编辑'
    },
    initialData: {
      type: Object,
      default: () => ({})
    }
  },
  setup(props) {
    // 表单数据
    const formData = reactive({
      name: props.initialData.name || '',
      price: props.initialData.price || 0,
      stock: props.initialData.stock || 0,
      category: props.initialData.category || '',
      tags: props.initialData.tags || []
    })
    
    // 可用标签
    const availableTags = ['热销', '新品', '推荐', '特价']
    
    // 验证错误
    const validationErrors = ref([])
    
    // 表单验证
    const isFormValid = computed(() => {
      const errors = []
      
      if (!formData.name.trim()) {
        errors.push('商品名称不能为空')
      }
      
      if (formData.price <= 0) {
        errors.push('价格必须大于0')
      }
      
      if (formData.stock < 0) {
        errors.push('库存数量不能为负数')
      }
      
      validationErrors.value = errors
      return errors.length === 0
    })
    
    // 切换标签
    const toggleTag = (tag) => {
      const index = formData.tags.indexOf(tag)
      if (index > -1) {
        formData.tags.splice(index, 1)
      } else {
        formData.tags.push(tag)
      }
    }
    
    // 保存表单
    const save = () => {
      if (isFormValid.value) {
        console.log('保存数据:', formData)
        // 这里可以调用 API 保存数据
        alert('数据保存成功!')
      }
    }
    
    // 重置表单
    const reset = () => {
      Object.assign(formData, props.initialData)
      validationErrors.value = []
    }
    
    return {
      formData,
      availableTags,
      validationErrors,
      isFormValid,
      toggleTag,
      save,
      reset
    }
  }
}
</script>

<style scoped>
.product-form {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin: 10px 0;
}

.form-group {
  margin-bottom: 15px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input,
.form-group select {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.tag-container {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 5px;
}

.tag {
  padding: 4px 8px;
  background-color: #f0f0f0;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.tag:hover {
  background-color: #e0e0e0;
}

.tag.active {
  background-color: #007bff;
  color: white;
}

.errors {
  margin-top: 10px;
  padding: 10px;
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
  color: #721c24;
}

button {
  margin-right: 10px;
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

组合式逻辑复用

自定义 Composable 函数

Composition API 的最大优势之一是能够将可复用的逻辑封装成自定义的 Composable 函数。让我们创建一个用户数据管理的 Composable:

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

export function useUser() {
  // 响应式状态
  const users = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  // 当前用户
  const currentUser = reactive({
    id: null,
    name: '',
    email: ''
  })
  
  // 计算属性
  const userCount = computed(() => users.value.length)
  
  // 方法
  const fetchUsers = async () => {
    loading.value = true
    error.value = null
    
    try {
      // 模拟 API 调用
      const response = await new Promise(resolve => {
        setTimeout(() => {
          resolve({
            data: [
              { id: 1, name: 'John', email: 'john@example.com' },
              { id: 2, name: 'Jane', email: 'jane@example.com' }
            ]
          })
        }, 1000)
      })
      
      users.value = response.data
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  const addUser = (userData) => {
    const newUser = {
      id: Date.now(),
      ...userData
    }
    users.value.push(newUser)
  }
  
  const updateUser = (id, userData) => {
    const index = users.value.findIndex(user => user.id === id)
    if (index > -1) {
      Object.assign(users.value[index], userData)
    }
  }
  
  const deleteUser = (id) => {
    users.value = users.value.filter(user => user.id !== id)
  }
  
  // 初始化
  fetchUsers()
  
  return {
    users,
    loading,
    error,
    currentUser,
    userCount,
    fetchUsers,
    addUser,
    updateUser,
    deleteUser
  }
}

使用自定义 Composable

现在让我们在组件中使用这个自定义的 Composable:

<template>
  <div class="user-management">
    <h2>用户管理</h2>
    
    <!-- 加载状态 -->
    <div v-if="loading">加载中...</div>
    
    <!-- 错误信息 -->
    <div v-else-if="error" class="error">{{ error }}</div>
    
    <!-- 用户列表 -->
    <div v-else>
      <p>总用户数: {{ userCount }}</p>
      
      <div class="user-list">
        <div 
          v-for="user in users" 
          :key="user.id"
          class="user-item"
        >
          <span>{{ user.name }} - {{ user.email }}</span>
          <button @click="deleteUser(user.id)">删除</button>
        </div>
      </div>
      
      <!-- 添加用户表单 -->
      <form @submit.prevent="handleSubmit" class="add-user-form">
        <input 
          v-model="newUser.name" 
          placeholder="用户名" 
          required
        />
        <input 
          v-model="newUser.email" 
          placeholder="邮箱" 
          type="email" 
          required
        />
        <button type="submit">添加用户</button>
      </form>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue'
import { useUser } from '@/composables/useUser'

export default {
  name: 'UserManagement',
  setup() {
    // 使用自定义 Composable
    const { 
      users, 
      loading, 
      error, 
      userCount,
      addUser,
      deleteUser 
    } = useUser()
    
    // 新用户数据
    const newUser = ref({
      name: '',
      email: ''
    })
    
    // 处理表单提交
    const handleSubmit = () => {
      if (newUser.value.name && newUser.value.email) {
        addUser({
          name: newUser.value.name,
          email: newUser.value.email
        })
        
        // 重置表单
        newUser.value = { name: '', email: '' }
      }
    }
    
    return {
      users,
      loading,
      error,
      userCount,
      newUser,
      handleSubmit,
      deleteUser
    }
  }
}
</script>

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

.user-list {
  margin: 20px 0;
}

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

.add-user-form {
  display: flex;
  gap: 10px;
  margin-top: 20px;
}

.add-user-form input {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.add-user-form button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.error {
  color: red;
  padding: 10px;
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
}
</style>

高级响应式编程技巧

响应式数据的深度监听

在 Vue 3 中,reactive 创建的对象是深层响应式的,这意味着对象内部的所有嵌套属性都会被监听:

import { reactive, watch } from 'vue'

const state = reactive({
  user: {
    profile: {
      name: 'John',
      age: 30
    }
  },
  settings: {
    theme: 'light',
    language: 'zh-CN'
  }
})

// 监听深层变化
watch(() => state.user.profile.name, (newVal, oldVal) => {
  console.log(`姓名从 ${oldVal} 变为 ${newVal}`)
})

// 修改嵌套属性会触发监听器
state.user.profile.name = 'Jane' // 输出: 姓名从 John 变为 Jane

使用 watchEffect 进行自动依赖追踪

watchEffect 会自动追踪其内部函数执行时使用的响应式数据,无需显式指定依赖:

import { ref, watchEffect } from 'vue'

const count = ref(0)
const doubleCount = ref(0)

// 自动追踪依赖
watchEffect(() => {
  doubleCount.value = count.value * 2
  console.log(`当前计数: ${count.value}, 双倍计数: ${doubleCount.value}`)
})

// 当 count 改变时,watchEffect 会自动重新执行
count.value++ // 输出: 当前计数: 1, 双倍计数: 2

条件性响应式数据

在某些场景下,我们可能需要根据条件创建响应式数据:

import { ref, computed } from 'vue'

export default {
  setup() {
    const showDetails = ref(false)
    
    // 根据条件创建响应式数据
    const userDetails = computed(() => {
      if (showDetails.value) {
        return {
          name: 'John',
          email: 'john@example.com',
          phone: '123-456-7890'
        }
      }
      return {
        name: 'John',
        email: 'john@example.com'
      }
    })
    
    // 可以在模板中使用
    return {
      showDetails,
      userDetails
    }
  }
}

性能优化最佳实践

合理使用响应式 API

// ❌ 不推荐:不必要的响应式包装
const data = ref({
  items: ['item1', 'item2', 'item3']
})

// ✅ 推荐:只对需要响应式变化的数据进行包装
const items = ref(['item1', 'item2', 'item3'])

// 对于不需要响应式变化的纯数据,可以使用普通对象
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
}

避免过度监听

import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const name = ref('')
    
    // ❌ 不推荐:监听不需要的响应式数据
    watch([count, name], () => {
      console.log('任何变化都会触发')
    })
    
    // ✅ 推荐:只监听需要的数据
    watch(count, (newVal) => {
      console.log(`计数变化: ${newVal}`)
    })
    
    return { count, name }
  }
}

使用 provide/inject 进行跨层级通信

// 父组件
import { provide, ref } from 'vue'

export default {
  setup() {
    const theme = ref('light')
    const user = ref({ name: 'John' })
    
    provide('theme', theme)
    provide('user', user)
    
    return { theme, user }
  }
}

// 子组件
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject('theme')
    const user = inject('user')
    
    return { theme, user }
  }
}

实际项目应用案例

电商商品列表组件

让我们构建一个完整的电商商品列表组件,展示如何在实际项目中应用 Composition API:

<template>
  <div class="product-list">
    <!-- 搜索和筛选 -->
    <div class="controls">
      <input 
        v-model="searchQuery" 
        placeholder="搜索商品..." 
        class="search-input"
      />
      
      <select v-model="selectedCategory" class="category-select">
        <option value="">所有分类</option>
        <option value="electronics">电子产品</option>
        <option value="clothing">服装</option>
        <option value="books">图书</option>
      </select>
      
      <button @click="clearFilters">清除筛选</button>
    </div>
    
    <!-- 商品列表 -->
    <div class="products-grid">
      <div 
        v-for="product in filteredProducts" 
        :key="product.id"
        class="product-card"
      >
        <img :src="product.image" :alt="product.name" />
        <h3>{{ product.name }}</h3>
        <p class="price">¥{{ product.price }}</p>
        <p class="category">{{ product.category }}</p>
        <button @click="addToCart(product)" class="add-to-cart">
          加入购物车
        </button>
      </div>
    </div>
    
    <!-- 分页 -->
    <div class="pagination">
      <button 
        :disabled="currentPage === 1" 
        @click="goToPage(currentPage - 1)"
      >
        上一页
      </button>
      
      <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
      
      <button 
        :disabled="currentPage === totalPages" 
        @click="goToPage(currentPage + 1)"
      >
        下一页
      </button>
    </div>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误信息 -->
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

<script>
import { ref, reactive, computed, watch } from 'vue'
import { useCart } from '@/composables/useCart'

export default {
  name: 'ProductList',
  setup() {
    // 响应式状态
    const products = ref([])
    const loading = ref(false)
    const error = ref(null)
    const searchQuery = ref('')
    const selectedCategory = ref('')
    
    // 分页状态
    const currentPage = ref(1)
    const pageSize = ref(12)
    
    // 购物车相关
    const { addToCart } = useCart()
    
    // 计算属性
    const filteredProducts = computed(() => {
      let result = products.value
      
      // 搜索过滤
      if (searchQuery.value) {
        const query = searchQuery.value.toLowerCase()
        result = result.filter(product => 
          product.name.toLowerCase().includes(query) ||
          product.description.toLowerCase().includes(query)
        )
      }
      
      // 分类过滤
      if (selectedCategory.value) {
        result = result.filter(product => 
          product.category === selectedCategory.value
        )
      }
      
      return result
    })
    
    const totalPages = computed(() => {
      return Math.ceil(filteredProducts.value.length / pageSize.value)
    })
    
    const paginatedProducts = computed(() => {
      const start = (currentPage.value - 1) * pageSize.value
      const end = start + pageSize.value
      return filteredProducts.value.slice(start, end)
    })
    
    // 方法
    const fetchProducts = async () => {
      loading.value = true
      error.value = null
      
      try {
        // 模拟 API 调用
        const response = await new Promise(resolve => {
          setTimeout(() => {
            resolve({
              data: [
                { id: 1, name: 'iPhone 13', price: 6999, category: 'electronics', image: '/images/iphone.jpg' },
                { id: 2, name: 'MacBook Pro', price: 12999, category: 'electronics', image: '/images/macbook.jpg' },
                { id: 3, name: 'T-Shirt', price: 99, category: 'clothing', image: '/images/tshirt.jpg' },
                { id: 4, name: 'JavaScript高级程序设计', price: 89, category: 'books', image: '/images/js-book.jpg' }
              ]
            })
          }, 1000)
        })
        
        products.value = response.data
      } catch (err) {
        error.value = err.message
      } finally {
        loading.value = false
      }
    }
    
    const clearFilters = () => {
      searchQuery.value = ''
      selectedCategory.value = ''
      currentPage.value = 1
    }
    
    const goToPage = (page) => {
      if (page >= 1 && page <= totalPages.value) {
        currentPage.value = page
      }
    }
    
    // 监听筛选条件变化
    watch([searchQuery, selectedCategory], () => {
      currentPage.value = 1 // 重置到第一页
    })
    
    // 初始化数据
    fetchProducts()
    
    return {
      products,
      loading,
      error,
      searchQuery,
      selectedCategory,
      currentPage,
      totalPages,
      paginatedProducts,
      addToCart,
      clearFilters,
      goToPage
    }
  }
}
</script>

<style scoped>
.product-list {
  padding: 20px;
}

.controls {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  align-items: center;
  flex-wrap: wrap;
}

.search-input,
.category-select {
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.products-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.product-card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 15px;
  text-align: center;
  transition: transform 0.2s;
}

.product-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.product-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 4px;
  margin-bottom: 10px;
}

.price {
  font-size
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000