Vue 3 Composition API 实战:从基础语法到复杂组件状态管理的完整指南

Zach434
Zach434 2026-01-29T11:16:29+08:00
0 0 4

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比传统的 Options API,Composition API 提供了更加灵活和强大的组件开发方式,特别是在处理复杂组件逻辑时展现出显著优势。本文将深入探讨 Vue 3 Composition API 的核心概念、使用技巧以及最佳实践,帮助开发者构建更灵活、可维护的 Vue 应用。

Vue 3 Composition API 核心概念

什么是 Composition API?

Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。它允许开发者将组件的相关逻辑按功能进行组合,而不是按照选项(如 data、methods、computed 等)来组织代码。这种组织方式更加灵活,特别是在处理复杂组件时,能够有效避免代码分散和重复的问题。

Composition API 的核心函数

Composition API 提供了一系列响应式函数,这些函数是构建组件逻辑的基础:

  • ref():创建响应式数据
  • reactive():创建响应式对象
  • computed():创建计算属性
  • watch():监听数据变化
  • watchEffect():自动追踪依赖的副作用函数

响应式数据管理

Ref 的使用

ref 是最基础的响应式数据创建函数,它可以为任何类型的值创建响应式引用。让我们通过一个简单的例子来理解:

import { ref, watch } from 'vue'

export default {
  setup() {
    // 创建基本类型响应式数据
    const count = ref(0)
    const message = ref('Hello Vue 3')
    
    // 修改数据
    const increment = () => {
      count.value++
    }
    
    // 监听变化
    watch(count, (newVal, oldVal) => {
      console.log(`count 从 ${oldVal} 变为 ${newVal}`)
    })
    
    return {
      count,
      message,
      increment
    }
  }
}

在模板中使用:

<template>
  <div>
    <p>计数: {{ count }}</p>
    <p>消息: {{ message }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

Reactive 的使用

reactive 用于创建响应式对象,它会将对象的所有属性都转换为响应式的:

import { reactive, watch } from 'vue'

export default {
  setup() {
    // 创建响应式对象
    const state = reactive({
      name: 'Vue',
      version: '3.0',
      isAwesome: true,
      userInfo: {
        age: 25,
        city: 'Beijing'
      }
    })
    
    const updateUserInfo = () => {
      state.userInfo.age++
      state.isAwesome = !state.isAwesome
    }
    
    // 监听对象变化
    watch(state, (newVal, oldVal) => {
      console.log('状态发生变化:', newVal)
    }, { deep: true })
    
    return {
      state,
      updateUserInfo
    }
  }
}

响应式数据的访问和修改

在 Composition API 中,访问响应式数据需要通过 .value 属性,而在模板中可以直接使用:

import { ref, reactive } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    // 在 setup 中修改数据
    const increment = () => {
      count.value++
      user.age++
    }
    
    // 返回给模板使用
    return {
      count,
      user,
      increment
    }
  }
}

生命周期钩子

使用生命周期钩子

Composition API 提供了与 Options API 对应的生命周期钩子函数:

import { onMounted, onUpdated, onUnmounted, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 组件挂载时执行
    onMounted(() => {
      console.log('组件已挂载')
      // 可以在这里进行 DOM 操作
      const timer = setInterval(() => {
        count.value++
      }, 1000)
      
      // 返回清理函数
      return () => {
        clearInterval(timer)
        console.log('清理定时器')
      }
    })
    
    // 组件更新时执行
    onUpdated(() => {
      console.log('组件已更新')
    })
    
    // 组件卸载前执行
    onUnmounted(() => {
      console.log('组件即将卸载')
    })
    
    return {
      count
    }
  }
}

生命周期钩子的最佳实践

import { 
  onMounted, 
  onUpdated, 
  onUnmounted, 
  watch, 
  ref 
} from 'vue'

export default {
  setup() {
    const data = ref(null)
    const loading = ref(false)
    
    // 挂载时获取数据
    onMounted(async () => {
      await fetchData()
    })
    
    // 监听数据变化
    watch(data, (newData) => {
      console.log('数据更新:', newData)
    })
    
    const fetchData = async () => {
      loading.value = true
      try {
        const response = await fetch('/api/data')
        data.value = await response.json()
      } catch (error) {
        console.error('获取数据失败:', error)
      } finally {
        loading.value = false
      }
    }
    
    return {
      data,
      loading,
      fetchData
    }
  }
}

计算属性和监听器

Computed 的使用

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 isAdult = computed(() => {
      return age.value >= 18
    })
    
    const userStatus = computed(() => {
      if (isAdult.value) {
        return '成年人'
      } else {
        return '未成年人'
      }
    })
    
    return {
      firstName,
      lastName,
      age,
      fullName,
      displayName,
      isAdult,
      userStatus
    }
  }
}

Watch 的使用

watch 函数用于监听数据变化,提供了丰富的配置选项:

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

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue')
    const userInfo = ref({
      age: 25,
      city: 'Beijing'
    })
    
    // 监听单个值
    watch(count, (newVal, oldVal) => {
      console.log(`count 从 ${oldVal} 变为 ${newVal}`)
    })
    
    // 监听多个值
    watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
      console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
    })
    
    // 深度监听对象
    watch(userInfo, (newVal, oldVal) => {
      console.log('用户信息变化:', newVal)
    }, { deep: true })
    
    // 立即执行
    watch(count, (newVal) => {
      console.log('立即执行:', newVal)
    }, { immediate: true })
    
    // 使用 watchEffect 自动追踪依赖
    const watchEffectExample = () => {
      watchEffect(() => {
        console.log(`姓名: ${name.value}, 年龄: ${userInfo.value.age}`)
        // 这里会自动追踪 name 和 userInfo 的变化
      })
    }
    
    return {
      count,
      name,
      userInfo,
      watchEffectExample
    }
  }
}

组合式函数复用

创建可复用的组合式函数

组合式函数是 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/useApi.js
import { ref, reactive } from 'vue'

export function useApi(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)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
      console.error('获取数据失败:', err)
    } finally {
      loading.value = false
    }
  }
  
  const refresh = () => {
    fetchData()
  }
  
  return {
    data,
    loading,
    error,
    fetchData,
    refresh
  }
}

// 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 !== null) {
    try {
      value.value = JSON.parse(initValue)
    } catch (e) {
      console.error('解析 localStorage 失败:', e)
    }
  }
  
  // 监听变化并同步到 localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

使用组合式函数

<template>
  <div>
    <h2>计数器示例</h2>
    <p>当前计数: {{ count }}</p>
    <p>双倍计数: {{ doubleCount }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
    
    <h2>API 数据示例</h2>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else-if="data">
      <pre>{{ JSON.stringify(data, null, 2) }}</pre>
    </div>
    <button @click="fetchData">获取数据</button>
    
    <h2>本地存储示例</h2>
    <input v-model="theme" placeholder="输入主题颜色">
    <p>当前主题: {{ theme }}</p>
  </div>
</template>

<script>
import { useCounter } from './composables/useCounter'
import { useApi } from './composables/useApi'
import { useLocalStorage } from './composables/useLocalStorage'

export default {
  setup() {
    // 使用计数器组合式函数
    const { count, increment, decrement, reset, doubleCount } = useCounter(10)
    
    // 使用 API 组合式函数
    const { data, loading, error, fetchData } = useApi('/api/users')
    
    // 使用本地存储组合式函数
    const theme = useLocalStorage('theme', '#ffffff')
    
    return {
      count,
      increment,
      decrement,
      reset,
      doubleCount,
      data,
      loading,
      error,
      fetchData,
      theme
    }
  }
}
</script>

复杂组件状态管理

状态管理的最佳实践

在复杂的组件中,合理的状态管理至关重要。我们可以结合组合式函数和响应式系统来构建健壮的状态管理:

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

export function useForm(initialData = {}) {
  const formData = reactive({ ...initialData })
  const errors = reactive({})
  const isSubmitting = ref(false)
  
  // 验证规则
  const validators = {}
  
  const setValidator = (field, validator) => {
    validators[field] = validator
  }
  
  const validateField = (field) => {
    if (validators[field]) {
      const error = validators[field](formData[field])
      errors[field] = error || ''
      return !error
    }
    return true
  }
  
  const validateAll = () => {
    let isValid = true
    Object.keys(validators).forEach(field => {
      if (!validateField(field)) {
        isValid = false
      }
    })
    return isValid
  }
  
  const clearErrors = () => {
    Object.keys(errors).forEach(key => {
      errors[key] = ''
    })
  }
  
  const submit = async (submitHandler) => {
    if (!validateAll()) {
      return false
    }
    
    isSubmitting.value = true
    try {
      await submitHandler(formData)
      clearErrors()
      return true
    } catch (error) {
      console.error('提交失败:', error)
      return false
    } finally {
      isSubmitting.value = false
    }
  }
  
  const reset = () => {
    Object.keys(formData).forEach(key => {
      formData[key] = initialData[key] || ''
    })
    clearErrors()
  }
  
  // 计算属性:检查是否有错误
  const hasErrors = computed(() => {
    return Object.values(errors).some(error => error !== '')
  })
  
  return {
    formData,
    errors,
    isSubmitting,
    hasErrors,
    setValidator,
    validateField,
    validateAll,
    submit,
    reset
  }
}

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

export function usePagination(totalItems = 0, itemsPerPage = 10) {
  const currentPage = ref(1)
  const pageSize = ref(itemsPerPage)
  
  const totalPages = computed(() => {
    return Math.ceil(totalItems / pageSize.value)
  })
  
  const startIndex = computed(() => {
    return (currentPage.value - 1) * pageSize.value
  })
  
  const endIndex = computed(() => {
    return Math.min(startIndex.value + pageSize.value, totalItems)
  })
  
  const hasNextPage = computed(() => {
    return currentPage.value < totalPages.value
  })
  
  const hasPrevPage = computed(() => {
    return currentPage.value > 1
  })
  
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  const nextPage = () => {
    if (hasNextPage.value) {
      currentPage.value++
    }
  }
  
  const prevPage = () => {
    if (hasPrevPage.value) {
      currentPage.value--
    }
  }
  
  const setPageSize = (size) => {
    pageSize.value = size
    currentPage.value = 1
  }
  
  return {
    currentPage,
    pageSize,
    totalPages,
    startIndex,
    endIndex,
    hasNextPage,
    hasPrevPage,
    goToPage,
    nextPage,
    prevPage,
    setPageSize
  }
}

实际应用示例

<template>
  <div class="form-container">
    <h2>用户注册表单</h2>
    
    <!-- 表单 -->
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label>用户名:</label>
        <input v-model="formData.username" type="text" />
        <span class="error" v-if="errors.username">{{ errors.username }}</span>
      </div>
      
      <div class="form-group">
        <label>邮箱:</label>
        <input v-model="formData.email" type="email" />
        <span class="error" v-if="errors.email">{{ errors.email }}</span>
      </div>
      
      <div class="form-group">
        <label>密码:</label>
        <input v-model="formData.password" type="password" />
        <span class="error" v-if="errors.password">{{ errors.password }}</span>
      </div>
      
      <button 
        type="submit" 
        :disabled="isSubmitting || hasErrors"
        class="submit-btn"
      >
        {{ isSubmitting ? '提交中...' : '注册' }}
      </button>
    </form>
    
    <!-- 分页器 -->
    <div class="pagination-container">
      <h3>用户列表</h3>
      <div class="user-list">
        <div 
          v-for="user in paginatedUsers" 
          :key="user.id"
          class="user-item"
        >
          {{ user.name }} - {{ user.email }}
        </div>
      </div>
      
      <div class="pagination-controls">
        <button @click="prevPage" :disabled="!hasPrevPage">上一页</button>
        <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
        <button @click="nextPage" :disabled="!hasNextPage">下一页</button>
      </div>
    </div>
    
    <!-- 提交结果显示 -->
    <div v-if="submitSuccess" class="success-message">
      注册成功!
    </div>
  </div>
</template>

<script>
import { usePagination } from './composables/usePagination'
import { useForm } from './composables/useForm'

export default {
  setup() {
    // 表单状态管理
    const { 
      formData, 
      errors, 
      isSubmitting, 
      hasErrors,
      setValidator,
      validateField,
      submit,
      reset 
    } = useForm({
      username: '',
      email: '',
      password: ''
    })
    
    // 设置验证规则
    setValidator('username', (value) => {
      if (!value) return '用户名不能为空'
      if (value.length < 3) return '用户名至少3个字符'
      return ''
    })
    
    setValidator('email', (value) => {
      if (!value) return '邮箱不能为空'
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
      if (!emailRegex.test(value)) return '邮箱格式不正确'
      return ''
    })
    
    setValidator('password', (value) => {
      if (!value) return '密码不能为空'
      if (value.length < 6) return '密码至少6个字符'
      return ''
    })
    
    // 分页状态管理
    const { 
      currentPage,
      totalPages,
      hasNextPage,
      hasPrevPage,
      nextPage,
      prevPage,
      startIndex,
      endIndex 
    } = usePagination(50, 10)
    
    // 模拟用户数据
    const users = Array.from({ length: 50 }, (_, i) => ({
      id: i + 1,
      name: `用户${i + 1}`,
      email: `user${i + 1}@example.com`
    }))
    
    const paginatedUsers = computed(() => {
      return users.slice(startIndex.value, endIndex.value)
    })
    
    // 提交处理
    const handleSubmit = async () => {
      const success = await submit(async (data) => {
        // 模拟 API 调用
        await new Promise(resolve => setTimeout(resolve, 1000))
        console.log('提交数据:', data)
        return { success: true }
      })
      
      if (success) {
        // 提交成功后的处理
        reset()
      }
    }
    
    const submitSuccess = ref(false)
    
    return {
      formData,
      errors,
      isSubmitting,
      hasErrors,
      currentPage,
      totalPages,
      hasNextPage,
      hasPrevPage,
      nextPage,
      prevPage,
      paginatedUsers,
      handleSubmit,
      submitSuccess
    }
  }
}
</script>

<style scoped>
.form-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

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

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

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

.error {
  color: red;
  font-size: 12px;
  margin-top: 5px;
}

.submit-btn {
  background-color: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.submit-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.pagination-container {
  margin-top: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 4px;
}

.user-item {
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.pagination-controls {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 15px;
  margin-top: 20px;
}

.success-message {
  background-color: #d4edda;
  color: #155724;
  padding: 15px;
  border-radius: 4px;
  margin-top: 20px;
}
</style>

性能优化技巧

合理使用响应式系统

// 避免不必要的响应式包装
import { ref, reactive, computed } from 'vue'

export default {
  setup() {
    // ✅ 好的做法:只对需要响应式的值使用 ref/reactive
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    // ❌ 避免:不必要的响应式包装
    // const notNeededReactive = reactive({ value: 'simple string' })
    
    // ✅ 好的做法:使用 computed 缓存计算结果
    const expensiveCalculation = computed(() => {
      // 模拟复杂计算
      let result = 0
      for (let i = 0; i < 1000000; i++) {
        result += Math.random()
      }
      return result
    })
    
    return {
      count,
      user,
      expensiveCalculation
    }
  }
}

避免循环依赖

// ❌ 避免:可能导致循环依赖的写法
import { ref, computed } from 'vue'

export default {
  setup() {
    const a = ref(1)
    const b = ref(2)
    
    // 这种写法可能导致循环依赖问题
    const computedA = computed(() => {
      return b.value + 1
    })
    
    const computedB = computed(() => {
      return a.value + 1
    })
    
    return {
      a,
      b,
      computedA,
      computedB
    }
  }
}

// ✅ 好的做法:避免循环依赖
export default {
  setup() {
    const a = ref(1)
    const b = ref(2)
    
    // 使用简单的计算逻辑
    const sum = computed(() => {
      return a.value + b.value
    })
    
    return {
      a,
      b,
      sum
    }
  }
}

最佳实践总结

组件结构优化

// 推荐的组件结构
import { 
  ref, 
  reactive, 
  computed, 
  watch, 
  onMounted, 
  onUnmounted 
} from 'vue'

export default {
  name: 'MyComponent',
  
  props: {
    title: String,
    data: Object
  },
  
  setup(props, { emit }) {
    // 1. 响应式数据声明
    const count = ref(0)
    const state = reactive({
      loading: false,
      error: null
    })
    
    // 2. 计算属性
    const displayTitle = computed(() => {
      return props.title || '默认标题'
    })
    
    const hasData = computed(() => {
      return props.data && Object.keys(props.data).length > 0
    })
    
    // 3. 方法定义
    const increment = () => {
      count.value++
    }
    
    const handleDataUpdate = (newData) => {
      emit('update:data', newData)
    }
    
    // 4. 生命周期钩子
    onMounted(() => {
      console.log('组件已挂载')
      // 初始化逻辑
    })
    
    onUnmounted(() => {
      console.log('组件即将卸载')
      // 清理逻辑
    })
    
    // 5. 监听器
    watch(count, (newVal) => {
      console.log('计数变化:', newVal)
    })
    
    // 6. 返回给模板使用的数据和方法
    return {
      count,
      state,
      displayTitle,
      hasData,
      increment,
      handleDataUpdate
    }
  }
}

错误处理和调试

import { ref, reactive, onErrorCaptured } from 'vue'

export default {
  setup() {
    const error = ref(null)
    
    // 全局错误捕获
    onErrorCaptured((err, instance, info) => {
      console.error('组件错误:', err, info)
      error.value = err.message
      return false // 阻止错误继续冒泡
    })
    
    // 异步操作的错误处理
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data')
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
        return await response.json()
      } catch (err) {
        console.error('数据获取失败:', err)
        error.value = err.message
        throw err // 重新抛出错误供上层处理
      }
    }
    
    return {
      error,
      fetchData
    }
  }
}

结论

Vue 3 Composition API 为前端开发者提供了更强大、更灵活的组件开发方式。通过合理使用 refreactivecomputedwatch 等核心函数,以及创建可复用的组合式函数,我们可以构建出更加清晰、可维护的组件。

在实际项目中

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000