Vue 3 Composition API实战:从组件通信到状态管理的完整开发指南

风吹麦浪
风吹麦浪 2026-01-28T07:10:14+08:00
0 0 1

前言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比于传统的 Options API,Composition API 提供了更灵活、更强大的组件开发方式。本文将深入探讨 Vue 3 Composition API 的核心特性,并通过实际案例展示如何在项目中应用这些技术。

Vue 3 Composition API 核心概念

什么是 Composition API

Composition API 是 Vue 3 中引入的一种新的组件开发模式,它允许我们使用函数来组织和复用组件逻辑。与传统的 Options API(data、methods、computed、watch 等选项)不同,Composition API 将组件的逻辑按照功能进行分组,使得代码更加模块化和可重用。

Composition API 的优势

  1. 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
  2. 更清晰的代码结构:将相关逻辑组织在一起,提高可读性
  3. 更灵活的开发方式:可以自由地组织和重组组件逻辑
  4. 更好的 TypeScript 支持:类型推断更加准确

响应式数据管理

reactive 和 ref 的基本使用

在 Composition API 中,响应式数据主要通过 reactiveref 两个函数来创建。

import { reactive, ref } from 'vue'

// 使用 ref 创建响应式数据
const count = ref(0)
const message = ref('Hello Vue 3')

// 使用 reactive 创建响应式对象
const state = reactive({
  name: 'Vue',
  version: 3,
  isAwesome: true
})

// 在模板中使用
// {{ count }} {{ message }} {{ state.name }}

ref 的深层应用

import { ref, watch } from 'vue'

export default {
  setup() {
    const user = ref({
      profile: {
        name: 'John',
        age: 25
      }
    })

    // 监听嵌套属性变化
    watch(() => user.value.profile.name, (newName, oldName) => {
      console.log(`Name changed from ${oldName} to ${newName}`)
    })

    const increment = () => {
      user.value.profile.age++
    }

    return {
      user,
      increment
    }
  }
}

computed 的高级用法

import { ref, computed } from 'vue'

export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    // 基础计算属性
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`
    })
    
    // 带有 getter 和 setter 的计算属性
    const reversedName = computed({
      get: () => {
        return fullName.value.split(' ').reverse().join(' ')
      },
      set: (newValue) => {
        const names = newValue.split(' ')
        firstName.value = names[0]
        lastName.value = names[1] || ''
      }
    })
    
    return {
      firstName,
      lastName,
      fullName,
      reversedName
    }
  }
}

组件间通信机制

Props 传递数据

// 父组件
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

export default {
  setup() {
    const message = ref('Hello from parent')
    const count = ref(10)
    
    return {
      message,
      count
    }
  }
}
<!-- 父组件模板 -->
<template>
  <div>
    <ChildComponent 
      :message="message" 
      :count="count"
      @child-event="handleChildEvent"
    />
  </div>
</template>
<!-- 子组件 -->
<script setup>
// 定义 props
const props = defineProps({
  message: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 定义 emits
const emit = defineEmits(['child-event'])

// 处理事件
const handleClick = () => {
  emit('child-event', 'Data from child')
}
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
    <button @click="handleClick">Send Event</button>
  </div>
</template>

emit 事件通信

<script setup>
const emit = defineEmits(['update:modelValue', 'custom-event'])

const handleInput = (value) => {
  // 触发更新事件
  emit('update:modelValue', value)
}

const handleCustomEvent = () => {
  emit('custom-event', { 
    timestamp: Date.now(),
    data: 'some data'
  })
}
</script>

provide 和 inject 的使用

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

export default {
  setup() {
    const theme = ref('dark')
    const user = ref({ name: 'John', role: 'admin' })
    
    // 提供数据
    provide('theme', theme)
    provide('user', user)
    
    const toggleTheme = () => {
      theme.value = theme.value === 'dark' ? 'light' : 'dark'
    }
    
    return {
      toggleTheme
    }
  }
}
<!-- 子组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
const theme = inject('theme')
const user = inject('user')

// 使用注入的数据
console.log(`Current theme: ${theme.value}`)
console.log(`User: ${user.value.name}`)
</script>

<template>
  <div :class="theme">
    <p>User: {{ user.name }}</p>
  </div>
</template>

状态管理最佳实践

使用 Composition API 实现简单状态管理

// store/userStore.js
import { ref, readonly } from 'vue'

const currentUser = ref(null)
const isLoggedIn = ref(false)

export function useUserStore() {
  const login = (userData) => {
    currentUser.value = userData
    isLoggedIn.value = true
  }
  
  const logout = () => {
    currentUser.value = null
    isLoggedIn.value = false
  }
  
  const updateProfile = (newData) => {
    if (currentUser.value) {
      currentUser.value = { ...currentUser.value, ...newData }
    }
  }
  
  return {
    // 只读状态
    currentUser: readonly(currentUser),
    isLoggedIn: readonly(isLoggedIn),
    
    // 方法
    login,
    logout,
    updateProfile
  }
}
<!-- 登录组件 -->
<script setup>
import { useUserStore } from '@/store/userStore'

const { login, isLoggedIn } = useUserStore()

const handleLogin = () => {
  login({
    name: 'John Doe',
    email: 'john@example.com',
    role: 'user'
  })
}
</script>

<template>
  <div>
    <button v-if="!isLoggedIn" @click="handleLogin">Login</button>
    <div v-else>Logged in as {{ currentUser.name }}</div>
  </div>
</template>

复杂状态管理的组合函数

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

export function useAsyncState(asyncFunction, initialValue = null) {
  const loading = ref(false)
  const error = ref(null)
  const data = ref(initialValue)
  
  const execute = async (...args) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await asyncFunction(...args)
      data.value = result
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = initialValue
    error.value = null
    loading.value = false
  }
  
  return {
    data: computed(() => data.value),
    loading: computed(() => loading.value),
    error: computed(() => error.value),
    execute,
    reset
  }
}
<!-- 数据获取组件 -->
<script setup>
import { useAsyncState } from '@/composables/useAsyncState'

const { data, loading, error, execute } = useAsyncState(fetchUserData, [])

const fetchUser = async (userId) => {
  await execute(`api/users/${userId}`)
}

// 组件加载时获取数据
execute('api/users/123')
</script>

<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">{{ error.message }}</div>
    <div v-else>{{ data }}</div>
  </div>
</template>

高级响应式特性

watch 的高级用法

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

export default {
  setup() {
    const count = ref(0)
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    // 基础监听器
    watch(count, (newVal, oldVal) => {
      console.log(`Count changed from ${oldVal} to ${newVal}`)
    })
    
    // 监听多个源
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
    })
    
    // 深度监听
    const user = ref({
      profile: {
        name: 'John',
        settings: {
          theme: 'dark'
        }
      }
    })
    
    watch(user, (newUser) => {
      console.log('User changed:', newUser)
    }, { deep: true })
    
    // 立即执行监听
    watch(count, (newVal) => {
      console.log(`Immediate watch: ${newVal}`)
    }, { immediate: true })
    
    // watchEffect
    const watchEffectExample = watchEffect(() => {
      console.log(`Count is now: ${count.value}`)
    })
    
    return {
      count,
      firstName,
      lastName
    }
  }
}

watchEffect 和 watch 的区别

// watchEffect 会立即执行,并自动追踪依赖
const watchEffectExample = watchEffect(() => {
  // 这里会立即执行一次
  console.log(`Current count: ${count.value}`)
  console.log(`Current name: ${firstName.value}`)
})

// watch 需要显式指定要监听的源
const watchExample = watch(count, (newVal) => {
  console.log(`Count changed to: ${newVal}`)
})

组件逻辑复用

自定义组合函数

// 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
}
<!-- 使用本地存储的组件 -->
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('app-theme', 'light')
const userPreferences = useLocalStorage('user-preferences', {
  notifications: true,
  language: 'en'
})
</script>

<template>
  <div :class="theme">
    <p>Current theme: {{ theme }}</p>
    <p>User preferences: {{ userPreferences }}</p>
  </div>
</template>

复杂逻辑的模块化

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

export function useFormValidation(initialData) {
  const formData = ref({ ...initialData })
  const errors = ref({})
  
  const isValid = computed(() => {
    return Object.keys(errors.value).length === 0
  })
  
  const validateField = (fieldName, value) => {
    // 简单的验证规则
    switch (fieldName) {
      case 'email':
        if (!value.includes('@')) {
          errors.value.email = 'Invalid email format'
        } else {
          delete errors.value.email
        }
        break
      case 'password':
        if (value.length < 6) {
          errors.value.password = 'Password must be at least 6 characters'
        } else {
          delete errors.value.password
        }
        break
    }
  }
  
  const validateForm = () => {
    Object.keys(formData.value).forEach(key => {
      validateField(key, formData.value[key])
    })
    return isValid.value
  }
  
  const setFieldValue = (field, value) => {
    formData.value[field] = value
    validateField(field, value)
  }
  
  return {
    formData,
    errors,
    isValid,
    validateForm,
    setFieldValue
  }
}
<!-- 表单组件 -->
<script setup>
import { useFormValidation } from '@/composables/useFormValidation'

const initialData = {
  email: '',
  password: ''
}

const { 
  formData, 
  errors, 
  isValid, 
  validateForm, 
  setFieldValue 
} = useFormValidation(initialData)

const handleSubmit = () => {
  if (validateForm()) {
    console.log('Form submitted:', formData.value)
  } else {
    console.log('Form has errors')
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input 
      v-model="formData.email" 
      type="email" 
      placeholder="Email"
      @blur="validateForm"
    />
    <div v-if="errors.email" class="error">{{ errors.email }}</div>
    
    <input 
      v-model="formData.password" 
      type="password" 
      placeholder="Password"
      @blur="validateForm"
    />
    <div v-if="errors.password" class="error">{{ errors.password }}</div>
    
    <button type="submit" :disabled="!isValid">Submit</button>
  </form>
</template>

性能优化技巧

使用 computed 缓存计算结果

import { ref, computed } from 'vue'

export default {
  setup() {
    const items = ref([])
    
    // 复杂的计算属性,使用 computed 进行缓存
    const expensiveCalculation = computed(() => {
      console.log('Computing expensive calculation...')
      return items.value.reduce((sum, item) => sum + item.value, 0)
    })
    
    const filteredItems = computed(() => {
      return items.value.filter(item => item.active)
    })
    
    return {
      items,
      expensiveCalculation,
      filteredItems
    }
  }
}

合理使用 watch 和 watchEffect

// 避免不必要的监听
export default {
  setup() {
    const count = ref(0)
    const name = ref('John')
    
    // 仅监听需要的值
    watch(count, (newVal) => {
      console.log(`Count changed: ${newVal}`)
    })
    
    // 避免在 watch 中进行复杂的计算
    watch(name, (newName) => {
      // 简单的副作用操作
      document.title = newName
    })
    
    return {
      count,
      name
    }
  }
}

实际项目应用案例

完整的购物车功能实现

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

export function useShoppingCart() {
  const items = ref([])
  
  const cartTotal = computed(() => {
    return items.value.reduce((total, item) => {
      return total + (item.price * item.quantity)
    }, 0)
  })
  
  const itemCount = computed(() => {
    return items.value.reduce((count, item) => count + item.quantity, 0)
  })
  
  const addToCart = (product) => {
    const existingItem = items.value.find(item => item.id === product.id)
    
    if (existingItem) {
      existingItem.quantity += 1
    } else {
      items.value.push({
        id: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      })
    }
  }
  
  const removeFromCart = (productId) => {
    items.value = items.value.filter(item => item.id !== productId)
  }
  
  const updateQuantity = (productId, newQuantity) => {
    if (newQuantity <= 0) {
      removeFromCart(productId)
      return
    }
    
    const item = items.value.find(item => item.id === productId)
    if (item) {
      item.quantity = newQuantity
    }
  }
  
  const clearCart = () => {
    items.value = []
  }
  
  return {
    items: computed(() => items.value),
    cartTotal,
    itemCount,
    addToCart,
    removeFromCart,
    updateQuantity,
    clearCart
  }
}
<!-- 购物车组件 -->
<script setup>
import { useShoppingCart } from '@/composables/useShoppingCart'

const { 
  items, 
  cartTotal, 
  itemCount, 
  addToCart, 
  removeFromCart, 
  updateQuantity 
} = useShoppingCart()

// 模拟产品数据
const products = [
  { id: 1, name: 'Product A', price: 29.99 },
  { id: 2, name: 'Product B', price: 39.99 },
  { id: 3, name: 'Product C', price: 49.99 }
]

const handleAddToCart = (product) => {
  addToCart(product)
}
</script>

<template>
  <div class="shopping-cart">
    <h2>Shopping Cart ({{ itemCount }} items)</h2>
    
    <div v-if="items.length === 0" class="empty-cart">
      Your cart is empty
    </div>
    
    <div v-else>
      <div 
        v-for="item in items" 
        :key="item.id"
        class="cart-item"
      >
        <span>{{ item.name }}</span>
        <span>${{ item.price }}</span>
        <input 
          type="number" 
          :value="item.quantity"
          @input="updateQuantity(item.id, $event.target.value)"
          min="1"
        />
        <button @click="removeFromCart(item.id)">Remove</button>
      </div>
      
      <div class="cart-total">
        Total: ${{ cartTotal.toFixed(2) }}
      </div>
    </div>
    
    <div class="products">
      <h3>Products</h3>
      <div 
        v-for="product in products" 
        :key="product.id"
        class="product"
      >
        <span>{{ product.name }}</span>
        <span>${{ product.price }}</span>
        <button @click="handleAddToCart(product)">Add to Cart</button>
      </div>
    </div>
  </div>
</template>

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

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

.cart-total {
  margin-top: 20px;
  font-size: 1.2em;
  font-weight: bold;
}

.products {
  margin-top: 30px;
}

.product {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}
</style>

最佳实践总结

代码组织原则

  1. 按功能分组:将相关逻辑组织在一起
  2. 合理使用组合函数:提取可复用的逻辑
  3. 保持响应式数据的简洁性:避免过度嵌套
  4. 明确的数据流向:确保状态变更清晰可追踪

性能优化建议

  1. 合理使用 computed:利用缓存机制
  2. 避免不必要的监听:只监听需要的值
  3. 及时清理副作用:在组件销毁时清理监听器
  4. 批量更新数据:减少不必要的重新渲染

开发工具和调试技巧

// 使用 Vue DevTools 进行调试
import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 在开发环境中添加调试信息
    if (process.env.NODE_ENV === 'development') {
      watch(count, (newVal, oldVal) => {
        console.log(`[DEBUG] Count changed: ${oldVal} -> ${newVal}`)
      })
    }
    
    return { count }
  }
}

结语

Vue 3 的 Composition API 为前端开发带来了更灵活、更强大的组件开发方式。通过合理使用 refreactivecomputedwatch 等核心特性,我们可以构建出更加模块化、可复用的组件逻辑。

在实际项目中,建议:

  • 从简单的组合函数开始,逐步增加复杂度
  • 充分利用 TypeScript 提供的类型安全
  • 建立统一的组合函数命名规范
  • 定期重构和优化现有代码

随着对 Composition API 理解的深入,开发者将能够构建出更加优雅、高效的 Vue 应用程序。记住,好的代码不仅功能完善,更要易于维护和扩展。通过持续实践和学习,我们可以在 Vue 3 的世界中创造出更多优秀的应用。

本文提供的示例和最佳实践希望能够帮助读者更好地理解和应用 Vue 3 Composition API,在实际开发中发挥其最大价值。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000