Vue 3 Composition API实战指南:从基础到复杂组件开发

MadFlower
MadFlower 2026-02-01T04:11:20+08:00
0 0 1

引言

Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。这个新特性彻底改变了我们编写Vue组件的方式,让代码更加灵活、可复用和易于维护。在本文中,我们将深入探讨Composition API的核心概念,并通过实际案例演示如何构建现代化的Vue组件。

Vue 3 Composition API基础概念

什么是Composition API

Composition API是Vue 3提供的一种新的组件逻辑组织方式。它允许我们以函数的形式组织组件逻辑,而不是传统的选项式API(Options API)。这种方式更加灵活,能够更好地处理复杂组件逻辑的复用和组合。

Composition API的核心函数

Composition API提供了多个核心函数来处理响应式数据、生命周期钩子等:

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

响应式数据管理

Ref vs Reactive

在Composition API中,我们主要使用refreactive来创建响应式数据:

import { ref, reactive } from 'vue'

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

// 使用reactive创建响应式对象
const state = reactive({
  count: 0,
  name: 'Vue',
  user: {
    firstName: 'John',
    lastName: 'Doe'
  }
})

// 在模板中使用时,ref需要通过.value访问
// 在组件逻辑中可以直接使用
console.log(count.value) // 0
console.log(state.count) // 0

复杂数据结构的响应式处理

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

export default {
  setup() {
    // 创建复杂的响应式对象
    const user = reactive({
      profile: {
        name: 'Alice',
        age: 25,
        email: 'alice@example.com'
      },
      preferences: {
        theme: 'dark',
        language: 'zh-CN'
      }
    })

    // 使用toRefs将响应式对象转换为可解构的ref
    const { profile, preferences } = toRefs(user)

    return {
      user,
      profile,
      preferences
    }
  }
}

生命周期钩子的使用

组件生命周期管理

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

export default {
  setup() {
    // 组件挂载前
    onBeforeMount(() => {
      console.log('组件即将挂载')
    })

    // 组件挂载后
    onMounted(() => {
      console.log('组件已挂载')
      // 可以在这里执行DOM操作
      const element = document.getElementById('myElement')
      if (element) {
        // 执行一些DOM相关的操作
      }
    })

    // 组件更新前
    onBeforeUpdate(() => {
      console.log('组件即将更新')
    })

    // 组件更新后
    onUpdated(() => {
      console.log('组件已更新')
    })

    // 组件卸载前
    onBeforeUnmount(() => {
      console.log('组件即将卸载')
    })

    // 组件卸载后
    onUnmounted(() => {
      console.log('组件已卸载')
    })

    return {}
  }
}

计算属性和监听器

计算属性的创建

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 userSummary = computed(() => {
      return `姓名: ${fullName.value}, 年龄: ${age.value}岁`
    })

    return {
      firstName,
      lastName,
      age,
      fullName,
      displayName,
      userSummary
    }
  }
}

监听器的使用

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

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue')
    const user = ref({ age: 25 })

    // 基本监听器
    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(user, (newVal, oldVal) => {
      console.log('user对象发生变化')
    }, { deep: true })

    // 立即执行的监听器
    watch(count, (newVal) => {
      console.log(`立即执行: ${newVal}`)
    }, { immediate: true })

    // watchEffect - 自动追踪依赖
    watchEffect(() => {
      console.log(`当前count值: ${count.value}`)
      console.log(`当前name值: ${name.value}`)
      // 这里会自动追踪count和name的依赖关系
    })

    return {
      count,
      name,
      user
    }
  }
}

组件逻辑复用

自定义组合函数

// 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 storedValue = localStorage.getItem(key)
  if (storedValue) {
    value.value = JSON.parse(storedValue)
  }
  
  // 监听值变化并同步到localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

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

export function useApi() {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async (url) => {
    try {
      loading.value = true
      error.value = null
      const response = await fetch(url)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return {
    data,
    loading,
    error,
    fetchData
  }
}

使用自定义组合函数

<template>
  <div>
    <h2>计数器示例</h2>
    <p>当前计数: {{ counter.count }}</p>
    <p>双倍计数: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">增加</button>
    <button @click="counter.decrement">减少</button>
    <button @click="counter.reset">重置</button>
    
    <h2>本地存储示例</h2>
    <input v-model="localStorageValue" placeholder="输入内容">
    <p>存储的值: {{ localStorageValue }}</p>
    
    <h2>API数据获取</h2>
    <div v-if="api.loading">加载中...</div>
    <div v-else-if="api.error">{{ api.error }}</div>
    <div v-else-if="api.data">
      <pre>{{ JSON.stringify(api.data, null, 2) }}</pre>
    </div>
    <button @click="api.fetchData('https://jsonplaceholder.typicode.com/posts/1')">
      获取数据
    </button>
  </div>
</template>

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

export default {
  setup() {
    // 使用计数器组合函数
    const counter = useCounter(0)
    
    // 使用本地存储组合函数
    const localStorageValue = useLocalStorage('myKey', '默认值')
    
    // 使用API组合函数
    const api = useApi()
    
    return {
      counter,
      localStorageValue,
      api
    }
  }
}
</script>

复杂组件开发实战

表单处理组件

<template>
  <form @submit.prevent="handleSubmit">
    <div class="form-group">
      <label>用户名:</label>
      <input 
        v-model="formData.username" 
        type="text" 
        :class="{ error: errors.username }"
      />
      <span v-if="errors.username" class="error-message">{{ errors.username }}</span>
    </div>
    
    <div class="form-group">
      <label>邮箱:</label>
      <input 
        v-model="formData.email" 
        type="email" 
        :class="{ error: errors.email }"
      />
      <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
    </div>
    
    <div class="form-group">
      <label>密码:</label>
      <input 
        v-model="formData.password" 
        type="password" 
        :class="{ error: errors.password }"
      />
      <span v-if="errors.password" class="error-message">{{ errors.password }}</span>
    </div>
    
    <div class="form-group">
      <label>确认密码:</label>
      <input 
        v-model="formData.confirmPassword" 
        type="password" 
        :class="{ error: errors.confirmPassword }"
      />
      <span v-if="errors.confirmPassword" class="error-message">{{ errors.confirmPassword }}</span>
    </div>
    
    <button type="submit" :disabled="isSubmitting">提交</button>
  </form>
</template>

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

export default {
  setup() {
    const formData = reactive({
      username: '',
      email: '',
      password: '',
      confirmPassword: ''
    })
    
    const errors = reactive({})
    const isSubmitting = ref(false)
    
    // 验证规则
    const validateField = (field, value) => {
      switch (field) {
        case 'username':
          if (!value) return '用户名不能为空'
          if (value.length < 3) return '用户名至少3个字符'
          break
        case 'email':
          if (!value) return '邮箱不能为空'
          const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
          if (!emailRegex.test(value)) return '邮箱格式不正确'
          break
        case 'password':
          if (!value) return '密码不能为空'
          if (value.length < 6) return '密码至少6个字符'
          break
        case 'confirmPassword':
          if (!value) return '请确认密码'
          if (value !== formData.password) return '两次输入的密码不一致'
          break
      }
      return ''
    }
    
    // 全局验证
    const validateForm = () => {
      const newErrors = {}
      
      Object.keys(formData).forEach(key => {
        const error = validateField(key, formData[key])
        if (error) {
          newErrors[key] = error
        }
      })
      
      return newErrors
    }
    
    // 实时验证
    const validate = (field) => {
      const error = validateField(field, formData[field])
      errors[field] = error
    }
    
    // 监听表单字段变化
    Object.keys(formData).forEach(key => {
      if (key !== 'confirmPassword') { // confirmPassword在验证时特殊处理
        watch(() => formData[key], () => {
          validate(key)
        })
      }
    })
    
    // 处理表单提交
    const handleSubmit = async () => {
      const newErrors = validateForm()
      
      if (Object.keys(newErrors).length > 0) {
        Object.assign(errors, newErrors)
        return
      }
      
      isSubmitting.value = true
      
      try {
        // 模拟API调用
        await new Promise(resolve => setTimeout(resolve, 1000))
        console.log('表单提交成功:', formData)
        // 这里可以添加实际的API调用逻辑
      } catch (error) {
        console.error('表单提交失败:', error)
      } finally {
        isSubmitting.value = false
      }
    }
    
    return {
      formData,
      errors,
      isSubmitting,
      handleSubmit
    }
  }
}
</script>

<style scoped>
.form-group {
  margin-bottom: 1rem;
}

.error-message {
  color: red;
  font-size: 0.8rem;
}

input.error {
  border-color: red;
}
</style>

数据表格组件

<template>
  <div class="data-table">
    <!-- 搜索和过滤 -->
    <div class="table-controls">
      <input 
        v-model="searchQuery" 
        placeholder="搜索..." 
        class="search-input"
      />
      <select v-model="filterStatus" class="filter-select">
        <option value="">全部状态</option>
        <option value="active">活跃</option>
        <option value="inactive">非活跃</option>
      </select>
    </div>
    
    <!-- 表格 -->
    <table class="table">
      <thead>
        <tr>
          <th v-for="column in columns" :key="column.key" @click="sort(column.key)">
            {{ column.title }}
            <span v-if="sortField === column.key" class="sort-indicator">
              {{ sortOrder === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in paginatedData" :key="row.id">
          <td v-for="column in columns" :key="column.key">
            {{ formatValue(row[column.key], column) }}
          </td>
        </tr>
      </tbody>
    </table>
    
    <!-- 分页 -->
    <div class="pagination">
      <button 
        @click="currentPage--" 
        :disabled="currentPage === 1"
        class="page-btn"
      >
        上一页
      </button>
      <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
      <button 
        @click="currentPage++" 
        :disabled="currentPage === totalPages"
        class="page-btn"
      >
        下一页
      </button>
    </div>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
  </div>
</template>

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

export default {
  props: {
    data: {
      type: Array,
      required: true
    },
    columns: {
      type: Array,
      required: true
    }
  },
  
  setup(props) {
    const searchQuery = ref('')
    const filterStatus = ref('')
    const sortField = ref('')
    const sortOrder = ref('asc')
    const currentPage = ref(1)
    const pageSize = ref(10)
    const loading = ref(false)
    
    // 过滤数据
    const filteredData = computed(() => {
      let result = [...props.data]
      
      if (searchQuery.value) {
        const query = searchQuery.value.toLowerCase()
        result = result.filter(item => 
          Object.values(item).some(value => 
            value.toString().toLowerCase().includes(query)
          )
        )
      }
      
      if (filterStatus.value) {
        result = result.filter(item => item.status === filterStatus.value)
      }
      
      return result
    })
    
    // 排序数据
    const sortedData = computed(() => {
      if (!sortField.value) return filteredData.value
      
      return [...filteredData.value].sort((a, b) => {
        const aValue = a[sortField.value]
        const bValue = b[sortField.value]
        
        if (typeof aValue === 'string') {
          const comparison = aValue.localeCompare(bValue)
          return sortOrder.value === 'asc' ? comparison : -comparison
        } else {
          const comparison = aValue - bValue
          return sortOrder.value === 'asc' ? comparison : -comparison
        }
      })
    })
    
    // 分页数据
    const totalPages = computed(() => {
      return Math.ceil(sortedData.value.length / pageSize.value)
    })
    
    const paginatedData = computed(() => {
      const start = (currentPage.value - 1) * pageSize.value
      return sortedData.value.slice(start, start + pageSize.value)
    })
    
    // 排序功能
    const sort = (field) => {
      if (sortField.value === field) {
        sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
      } else {
        sortField.value = field
        sortOrder.value = 'asc'
      }
    }
    
    // 格式化值
    const formatValue = (value, column) => {
      if (column.formatter) {
        return column.formatter(value)
      }
      
      if (typeof value === 'boolean') {
        return value ? '是' : '否'
      }
      
      return value
    }
    
    // 监听分页变化
    watch(currentPage, () => {
      currentPage.value = Math.max(1, Math.min(currentPage.value, totalPages.value))
    })
    
    // 监听数据变化时重置分页
    watch(() => props.data, () => {
      currentPage.value = 1
    })
    
    return {
      searchQuery,
      filterStatus,
      sortField,
      sortOrder,
      currentPage,
      pageSize,
      loading,
      totalPages,
      paginatedData,
      sort,
      formatValue
    }
  }
}
</script>

<style scoped>
.data-table {
  padding: 1rem;
}

.table-controls {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
  align-items: center;
}

.search-input, .filter-select {
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

.table th,
.table td {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid #ddd;
}

.table th {
  background-color: #f5f5f5;
  cursor: pointer;
  user-select: none;
}

.sort-indicator {
  margin-left: 0.25rem;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  margin-top: 1rem;
}

.page-btn {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background-color: white;
  cursor: pointer;
  border-radius: 4px;
}

.page-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.loading {
  text-align: center;
  padding: 2rem;
}
</style>

性能优化最佳实践

避免不必要的重渲染

import { ref, computed, shallowRef, markRaw } from 'vue'

export default {
  setup() {
    // 使用shallowRef进行浅层响应式
    const shallowData = shallowRef({
      name: 'Vue',
      version: '3.0'
    })
    
    // 对于不希望被响应式的对象,使用markRaw
    const rawData = markRaw({
      id: 1,
      name: 'Vue',
      handler: function() {
        console.log('处理事件')
      }
    })
    
    // 使用computed缓存计算结果
    const expensiveComputation = computed(() => {
      // 模拟耗时计算
      let result = 0
      for (let i = 0; i < 1000000; i++) {
        result += Math.random()
      }
      return result
    })
    
    // 使用watch的回调控制执行时机
    const watchOptions = {
      flush: 'post', // 在组件更新后执行
      deep: true,
      immediate: false
    }
    
    return {
      shallowData,
      rawData,
      expensiveComputation
    }
  }
}

组件通信优化

<template>
  <div>
    <h2>优化的组件通信</h2>
    <p>父组件数据: {{ parentData }}</p>
    <ChildComponent 
      :data="parentData" 
      @update-data="handleUpdate"
    />
  </div>
</template>

<script>
import { ref, watch } from 'vue'
import ChildComponent from './ChildComponent.vue'

export default {
  components: {
    ChildComponent
  },
  
  setup() {
    const parentData = ref('初始数据')
    
    // 使用watch监听数据变化,避免不必要的重新计算
    watch(parentData, (newVal, oldVal) => {
      console.log(`父组件数据从 ${oldVal} 变为 ${newVal}`)
    }, { deep: true })
    
    const handleUpdate = (data) => {
      parentData.value = data
    }
    
    return {
      parentData,
      handleUpdate
    }
  }
}
</script>

错误处理和调试

统一错误处理机制

import { ref, reactive } from 'vue'

export function useErrorHandler() {
  const error = ref(null)
  const loading = ref(false)
  
  const handleError = (error) => {
    console.error('组件错误:', error)
    // 可以在这里添加错误上报逻辑
    error.value = error.message || '发生未知错误'
  }
  
  const handleAsyncOperation = async (operation) => {
    try {
      loading.value = true
      error.value = null
      return await operation()
    } catch (err) {
      handleError(err)
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    error,
    loading,
    handleAsyncOperation
  }
}

// 在组件中使用
export default {
  setup() {
    const { error, loading, handleAsyncOperation } = useErrorHandler()
    
    const fetchData = async () => {
      await handleAsyncOperation(async () => {
        // 模拟API调用
        const response = await fetch('/api/data')
        if (!response.ok) {
          throw new Error('网络请求失败')
        }
        return response.json()
      })
    }
    
    return {
      error,
      loading,
      fetchData
    }
  }
}

总结

Vue 3的Composition API为前端开发带来了革命性的变化。通过本文的介绍,我们看到了:

  1. 响应式数据管理refreactive提供了灵活的数据响应式处理方式
  2. 生命周期钩子:更清晰的组件生命周期管理
  3. 逻辑复用:通过自定义组合函数实现代码复用
  4. 复杂组件开发:从表单到数据表格等实际场景的应用
  5. 性能优化:合理使用响应式特性避免性能问题
  6. 错误处理:建立统一的错误处理机制

Composition API的核心优势在于它让我们能够以更自然的方式组织代码逻辑,特别是在处理复杂的业务逻辑时,能够显著提升代码的可维护性和复用性。通过合理运用这些技术,我们可以构建出更加现代化、高效且易于维护的Vue应用。

在实际项目中,建议根据具体需求选择合适的API风格,同时注意遵循最佳实践,这样才能充分发挥Vue 3 Composition API的强大功能。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000