Vue 3 Composition API实战:从基础到复杂组件状态管理

Eve114
Eve114 2026-02-26T08:16:04+08:00
0 0 0

引言

Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。作为Vue 3的核心特性之一,Composition API为开发者提供了更加灵活和强大的组件状态管理方式。相比于Vue 2的Options API,Composition API让我们能够更好地组织和复用代码逻辑,特别是在处理复杂组件时展现出巨大的优势。

本文将深入探讨Vue 3 Composition API的核心特性和使用方法,从基础概念到高级应用,通过实际项目案例展示如何构建可维护的现代化Vue应用。我们将涵盖响应式数据处理、组合函数复用、组件通信等关键知识点,帮助开发者全面掌握这一重要技术。

什么是Composition API

核心概念

Composition API是Vue 3中引入的一种新的组件逻辑组织方式。它允许我们将组件的逻辑按照功能进行分组,而不是按照选项类型进行分组。这种组织方式使得代码更加清晰、可维护,并且能够更好地复用逻辑代码。

在传统的Options API中,我们按照datamethodscomputedwatch等选项来组织代码。而Composition API则允许我们将相关的逻辑组合在一起,形成更加模块化的代码结构。

与Options API的对比

让我们通过一个简单的例子来对比两种API的差异:

// Options API
export default {
  data() {
    return {
      count: 0,
      name: 'Vue'
    }
  },
  computed: {
    doubledCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    }
  }
}

// Composition API
import { ref, computed, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue')
    
    const doubledCount = computed(() => count.value * 2)
    
    const increment = () => {
      count.value++
    }
    
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    return {
      count,
      name,
      doubledCount,
      increment
    }
  }
}

响应式数据处理

ref和reactive的基础使用

Composition API的核心是响应式系统,它提供了两种主要的响应式数据处理方式:refreactive

ref的使用

ref用于创建响应式的数据,适用于基本数据类型和对象:

import { ref } from 'vue'

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

// 对象类型
const user = ref({
  name: 'John',
  age: 30
})

// 访问和修改
console.log(count.value) // 0
count.value = 10
console.log(count.value) // 10

// 对象属性访问
console.log(user.value.name) // John
user.value.name = 'Jane'

reactive的使用

reactive用于创建响应式对象,适用于复杂的数据结构:

import { reactive } from 'vue'

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

// 直接修改属性
state.count = 10
state.user.name = 'Jane'
state.todos.push({ id: 1, text: 'Learn Vue' })

// 由于是响应式对象,所有修改都会触发更新

响应式数据的深度处理

对于深层嵌套的对象,reactive会自动处理所有层级的响应式:

import { reactive } from 'vue'

const state = reactive({
  user: {
    profile: {
      name: 'John',
      settings: {
        theme: 'dark',
        language: 'en'
      }
    }
  }
})

// 深层修改
state.user.profile.settings.theme = 'light'
// 这会触发响应式更新

ref vs reactive的使用场景

// 使用ref的场景
const count = ref(0) // 基本数据类型
const name = ref('Vue') // 字符串
const isVisible = ref(false) // 布尔值

// 使用reactive的场景
const state = reactive({
  user: {
    name: 'John',
    age: 30
  },
  todos: [],
  loading: false
})

// 复杂对象结构
const complexData = reactive({
  items: [],
  filters: {
    category: '',
    search: ''
  },
  pagination: {
    page: 1,
    limit: 10,
    total: 0
  }
})

组合函数复用

创建可复用的组合函数

组合函数是Composition API的核心优势之一,它允许我们将可复用的逻辑封装成函数:

// composables/useCounter.js
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

// 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
    }
  }
  
  watch(url, fetchData, { immediate: true })
  
  return {
    data,
    loading,
    error,
    fetchData
  }
}

组合函数的实际应用

// components/Counter.vue
import { defineComponent } from 'vue'
import { useCounter } from '@/composables/useCounter'

export default defineComponent({
  name: 'Counter',
  setup() {
    const { count, increment, decrement, reset } = useCounter(0)
    
    return {
      count,
      increment,
      decrement,
      reset
    }
  }
})

// components/UserList.vue
import { defineComponent } from 'vue'
import { useFetch } from '@/composables/useFetch'

export default defineComponent({
  name: 'UserList',
  setup() {
    const { data: users, loading, error, fetchData } = useFetch('/api/users')
    
    return {
      users,
      loading,
      error,
      fetchData
    }
  }
})

高级组合函数示例

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

export function useLocalStorage(key, defaultValue) {
  const value = ref(defaultValue)
  
  // 从localStorage初始化
  const initValue = localStorage.getItem(key)
  if (initValue) {
    try {
      value.value = JSON.parse(initValue)
    } catch (e) {
      console.error('Failed to parse localStorage value:', e)
    }
  }
  
  // 监听value变化并同步到localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

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

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value)
  
  watch(value, (newValue) => {
    const handler = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
    
    return () => clearTimeout(handler)
  })
  
  return debouncedValue
}

组件通信

父子组件通信

在Composition API中,父子组件通信仍然保持简单直观:

// Parent.vue
import { defineComponent, ref } from 'vue'
import Child from './Child.vue'

export default defineComponent({
  name: 'Parent',
  setup() {
    const message = ref('Hello from parent')
    const count = ref(0)
    
    const handleChildEvent = (data) => {
      console.log('Received from child:', data)
      count.value++
    }
    
    return {
      message,
      count,
      handleChildEvent
    }
  },
  components: {
    Child
  }
})

// Child.vue
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Child',
  props: {
    message: {
      type: String,
      default: ''
    }
  },
  setup(props, { emit }) {
    const handleClick = () => {
      emit('child-event', { message: 'Hello from child', timestamp: Date.now() })
    }
    
    return {
      handleClick
    }
  }
})

兄弟组件通信

对于兄弟组件之间的通信,可以使用组合函数来实现:

// composables/useEventBus.js
import { ref } from 'vue'

const events = ref({})

export function useEventBus() {
  const on = (event, callback) => {
    if (!events.value[event]) {
      events.value[event] = []
    }
    events.value[event].push(callback)
  }
  
  const emit = (event, data) => {
    if (events.value[event]) {
      events.value[event].forEach(callback => callback(data))
    }
  }
  
  const off = (event, callback) => {
    if (events.value[event]) {
      events.value[event] = events.value[event].filter(cb => cb !== callback)
    }
  }
  
  return {
    on,
    emit,
    off
  }
}

// 使用示例
// ComponentA.vue
import { defineComponent } from 'vue'
import { useEventBus } from '@/composables/useEventBus'

export default defineComponent({
  setup() {
    const { emit } = useEventBus()
    
    const sendMessage = () => {
      emit('message-event', 'Hello from Component A')
    }
    
    return {
      sendMessage
    }
  }
})

// ComponentB.vue
import { defineComponent } from 'vue'
import { useEventBus } from '@/composables/useEventBus'

export default defineComponent({
  setup() {
    const { on } = useEventBus()
    const receivedMessage = ref('')
    
    on('message-event', (data) => {
      receivedMessage.value = data
    })
    
    return {
      receivedMessage
    }
  }
})

复杂组件状态管理

多状态管理示例

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

export function useForm(initialData = {}) {
  const formData = reactive({ ...initialData })
  const errors = ref({})
  const isSubmitting = ref(false)
  const isValid = computed(() => Object.keys(errors.value).length === 0)
  
  const setField = (field, value) => {
    formData[field] = value
    // 清除对应字段的错误
    if (errors.value[field]) {
      delete errors.value[field]
    }
  }
  
  const validateField = (field, value) => {
    // 简单的验证规则
    if (!value && field !== 'optionalField') {
      errors.value[field] = `${field} is required`
      return false
    }
    if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
      errors.value[field] = 'Email is invalid'
      return false
    }
    delete errors.value[field]
    return true
  }
  
  const validateAll = () => {
    Object.keys(formData).forEach(field => {
      validateField(field, formData[field])
    })
    return isValid.value
  }
  
  const submit = async (submitFn) => {
    if (!validateAll()) return false
    
    isSubmitting.value = true
    try {
      const result = await submitFn(formData)
      return result
    } catch (error) {
      console.error('Form submission failed:', error)
      return false
    } finally {
      isSubmitting.value = false
    }
  }
  
  const reset = () => {
    Object.keys(formData).forEach(key => {
      formData[key] = initialData[key] || ''
    })
    errors.value = {}
  }
  
  return {
    formData,
    errors,
    isSubmitting,
    isValid,
    setField,
    validateField,
    validateAll,
    submit,
    reset
  }
}

状态管理的实际应用

<template>
  <form @submit.prevent="handleSubmit">
    <div class="form-group">
      <label for="name">Name</label>
      <input 
        id="name" 
        v-model="formData.name" 
        @blur="validateField('name', formData.name)"
        :class="{ 'error': errors.name }"
      >
      <span v-if="errors.name" class="error-text">{{ errors.name }}</span>
    </div>
    
    <div class="form-group">
      <label for="email">Email</label>
      <input 
        id="email" 
        v-model="formData.email" 
        @blur="validateField('email', formData.email)"
        :class="{ 'error': errors.email }"
      >
      <span v-if="errors.email" class="error-text">{{ errors.email }}</span>
    </div>
    
    <button type="submit" :disabled="isSubmitting">
      {{ isSubmitting ? 'Submitting...' : 'Submit' }}
    </button>
    
    <div v-if="submitResult" class="submit-result">
      {{ submitResult }}
    </div>
  </form>
</template>

<script>
import { defineComponent } from 'vue'
import { useForm } from '@/composables/useForm'

export default defineComponent({
  name: 'UserForm',
  setup() {
    const { 
      formData, 
      errors, 
      isSubmitting, 
      setField, 
      validateField, 
      submit 
    } = useForm({
      name: '',
      email: ''
    })
    
    const submitHandler = async (data) => {
      // 模拟API调用
      await new Promise(resolve => setTimeout(resolve, 1000))
      return 'User created successfully!'
    }
    
    const handleSubmit = async () => {
      const result = await submit(submitHandler)
      if (result) {
        submitResult.value = result
      }
    }
    
    return {
      formData,
      errors,
      isSubmitting,
      setField,
      validateField,
      handleSubmit
    }
  }
})
</script>

高级特性和最佳实践

生命周期钩子的使用

Composition API提供了与Vue 2相同的生命周期钩子,但以函数形式提供:

import { 
  onMounted, 
  onUpdated, 
  onUnmounted, 
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount
} from 'vue'

export default {
  setup() {
    onBeforeMount(() => {
      console.log('Before mount')
    })
    
    onMounted(() => {
      console.log('Mounted')
      // 可以在这里初始化第三方库
    })
    
    onBeforeUpdate(() => {
      console.log('Before update')
    })
    
    onUpdated(() => {
      console.log('Updated')
    })
    
    onBeforeUnmount(() => {
      console.log('Before unmount')
    })
    
    onUnmounted(() => {
      console.log('Unmounted')
      // 清理定时器、事件监听器等
    })
    
    return {}
  }
}

响应式数据的高级用法

// 使用toRefs和toRef
import { ref, reactive, toRefs, toRef } from 'vue'

export default {
  setup() {
    const state = reactive({
      name: 'Vue',
      age: 30,
      email: 'vue@example.com'
    })
    
    // 将响应式对象转换为ref
    const { name, age, email } = toRefs(state)
    
    // 或者单独转换某个属性
    const nameRef = toRef(state, 'name')
    
    return {
      name,
      age,
      email,
      nameRef
    }
  }
}

// 使用watchEffect
import { ref, watchEffect } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubleCount = ref(0)
    
    // watchEffect会自动追踪依赖
    watchEffect(() => {
      doubleCount.value = count.value * 2
      console.log('Count changed:', count.value)
    })
    
    return {
      count,
      doubleCount
    }
  }
}

性能优化技巧

// 使用computed的缓存
import { ref, computed } from 'vue'

export default {
  setup() {
    const items = ref([])
    const filterText = ref('')
    
    // 使用computed进行缓存
    const filteredItems = computed(() => {
      return items.value.filter(item => 
        item.name.toLowerCase().includes(filterText.value.toLowerCase())
      )
    })
    
    // 对于复杂计算,可以使用缓存
    const expensiveValue = computed(() => {
      // 模拟复杂计算
      return items.value.reduce((acc, item) => {
        return acc + item.value * 2
      }, 0)
    })
    
    return {
      items,
      filterText,
      filteredItems,
      expensiveValue
    }
  }
}

// 使用memoization
import { ref, computed } from 'vue'

export function useMemoized(fn, deps) {
  const cache = ref(null)
  const lastDeps = ref(deps)
  
  return computed(() => {
    const shouldUpdate = !lastDeps.value.every((dep, index) => dep === deps[index])
    
    if (shouldUpdate) {
      cache.value = fn()
      lastDeps.value = deps
    }
    
    return cache.value
  })
}

实际项目案例

电商商品列表组件

<template>
  <div class="product-list">
    <div class="controls">
      <input 
        v-model="searchQuery" 
        placeholder="Search products..."
        class="search-input"
      >
      <select v-model="selectedCategory" class="category-select">
        <option value="">All Categories</option>
        <option v-for="category in categories" :key="category" :value="category">
          {{ category }}
        </option>
      </select>
    </div>
    
    <div v-if="loading" class="loading">Loading...</div>
    
    <div v-else-if="error" class="error">{{ error }}</div>
    
    <div v-else class="products-grid">
      <ProductCard 
        v-for="product in filteredProducts" 
        :key="product.id"
        :product="product"
        @add-to-cart="handleAddToCart"
      />
    </div>
    
    <div class="pagination" v-if="totalPages > 1">
      <button 
        @click="goToPage(currentPage - 1)" 
        :disabled="currentPage === 1"
      >
        Previous
      </button>
      <span>{{ currentPage }} of {{ totalPages }}</span>
      <button 
        @click="goToPage(currentPage + 1)" 
        :disabled="currentPage === totalPages"
      >
        Next
      </button>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, computed, watch } from 'vue'
import ProductCard from './ProductCard.vue'
import { useFetch } from '@/composables/useFetch'
import { useDebounce } from '@/composables/useDebounce'

export default defineComponent({
  name: 'ProductList',
  components: {
    ProductCard
  },
  setup() {
    const searchQuery = ref('')
    const selectedCategory = ref('')
    const currentPage = ref(1)
    const pageSize = ref(12)
    
    // 使用组合函数获取数据
    const { data: products, loading, error } = useFetch(
      () => `/api/products?page=${currentPage.value}&limit=${pageSize.value}`
    )
    
    const categories = computed(() => {
      if (!products.value) return []
      const uniqueCategories = [...new Set(products.value.map(p => p.category))]
      return uniqueCategories.sort()
    })
    
    // 防抖搜索
    const debouncedSearch = useDebounce(searchQuery, 300)
    
    // 过滤产品
    const filteredProducts = computed(() => {
      if (!products.value) return []
      
      let filtered = products.value
      
      // 搜索过滤
      if (debouncedSearch.value) {
        filtered = filtered.filter(product => 
          product.name.toLowerCase().includes(debouncedSearch.value.toLowerCase()) ||
          product.description.toLowerCase().includes(debouncedSearch.value.toLowerCase())
        )
      }
      
      // 分类过滤
      if (selectedCategory.value) {
        filtered = filtered.filter(product => 
          product.category === selectedCategory.value
        )
      }
      
      return filtered
    })
    
    const totalPages = computed(() => {
      if (!products.value) return 1
      return Math.ceil(filteredProducts.value.length / pageSize.value)
    })
    
    // 分页处理
    const goToPage = (page) => {
      if (page >= 1 && page <= totalPages.value) {
        currentPage.value = page
      }
    }
    
    // 添加到购物车
    const handleAddToCart = (product) => {
      console.log('Added to cart:', product)
      // 实际的购物车逻辑
    }
    
    // 监听搜索和分类变化
    watch([debouncedSearch, selectedCategory], () => {
      currentPage.value = 1
    })
    
    return {
      searchQuery,
      selectedCategory,
      currentPage,
      pageSize,
      products,
      loading,
      error,
      categories,
      filteredProducts,
      totalPages,
      goToPage,
      handleAddToCart
    }
  }
})
</script>

用户管理系统

<template>
  <div class="user-management">
    <div class="toolbar">
      <button @click="showCreateForm = true" class="btn btn-primary">
        Add User
      </button>
      <input 
        v-model="searchTerm" 
        placeholder="Search users..."
        class="search-input"
      >
    </div>
    
    <UserForm 
      v-if="showCreateForm"
      @submit="handleCreateUser"
      @cancel="showCreateForm = false"
    />
    
    <div class="users-list">
      <UserItem 
        v-for="user in filteredUsers" 
        :key="user.id"
        :user="user"
        @edit="handleEditUser"
        @delete="handleDeleteUser"
      />
    </div>
    
    <div class="pagination">
      <button 
        @click="currentPage--" 
        :disabled="currentPage === 1"
      >
        Previous
      </button>
      <span>{{ currentPage }} of {{ totalPages }}</span>
      <button 
        @click="currentPage++" 
        :disabled="currentPage === totalPages"
      >
        Next
      </button>
    </div>
  </div>
</template>

<script>
import { defineComponent, ref, computed, watch } from 'vue'
import UserForm from './UserForm.vue'
import UserItem from './UserItem.vue'
import { useFetch } from '@/composables/useFetch'
import { useLocalStorage } from '@/composables/useLocalStorage'

export default defineComponent({
  name: 'UserManagement',
  components: {
    UserForm,
    UserItem
  },
  setup() {
    const searchTerm = ref('')
    const currentPage = ref(1)
    const pageSize = useLocalStorage('user-page-size', 10)
    const showCreateForm = ref(false)
    
    const { data: users, loading, error, fetchData } = useFetch('/api/users')
    
    const filteredUsers = computed(() => {
      if (!users.value) return []
      
      let filtered = users.value
      
      if (searchTerm.value) {
        filtered = filtered.filter(user => 
          user.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
          user.email.toLowerCase().includes(searchTerm.value.toLowerCase())
        )
      }
      
      return filtered
    })
    
    const totalPages = computed(() => {
      if (!filteredUsers.value) return 1
      return Math.ceil(filteredUsers.value.length / pageSize.value)
    })
    
    const paginatedUsers = computed(() => {
      if (!filteredUsers.value) return []
      
      const start = (currentPage.value - 1) * pageSize.value
      const end = start + pageSize.value
      return filteredUsers.value.slice(start, end)
    })
    
    const handleCreateUser = async (userData) => {
      try {
        const response = await fetch('/api/users', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(userData)
        })
        
        const newUser = await response.json()
        // 更新用户列表
        if (users.value) {
          users.value.push(newUser)
        }
        showCreateForm.value = false
      } catch (error) {
        console.error('Failed to create user:', error)
      }
    }
    
    const handleEditUser = (user) => {
      // 编辑用户逻辑
      console.log('Edit user:', user)
    }
    
    const handleDeleteUser = async (userId) => {
      if (confirm('Are you sure you want to delete this user?')) {
        try {
          await fetch(`/api/users/${userId}`, {
            method: 'DELETE'
          })
          
          // 从列表中移除用户
          if (users.value) {
            users.value = users.value.filter(user => user.id !== userId)
          }
        } catch (error) {
          console.error('Failed to delete user:', error)
        }
      }
    }
    
    // 监听分页变化
    watch([currentPage, pageSize], () => {
      fetchData()
    })
    
    return {
      searchTerm,
      currentPage,
      pageSize,
      showCreateForm,
      users,
      loading,
      error,
      filteredUsers,
      totalPages,
      paginatedUsers,
      handleCreateUser,
      handleEditUser,
      handleDeleteUser
    }
  }
})
</script>

最佳实践总结

代码组织原则

  1. 按功能分组:将相关的逻辑组织在一起,而不是按选项类型分组
  2. 组合函数复用:将可复用的逻辑封装成组合函数
  3. 清晰的返回值:明确地返回需要在模板中使用的变量和方法

性能优化建议

  1. 合理使用computed:利用计算属性的缓存机制
  2. 避免不必要的监听:使用watchEffect自动追踪依赖
  3. 及时清理资源:在组件销毁时清理定时器和事件监听器

开发工具和调试

// 开发环境下的调试工具
import { watch } from 'vue'

export function useDebug(name, data) {
  if (process.env.NODE_ENV === 'development') {
    watch(data, (newVal, oldVal) => {
      console.log(`${name}
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000