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

Tara843
Tara843 2026-02-04T14:08:09+08:00
0 0 1

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。作为 Vue 3 的核心特性之一,Composition API 为开发者提供了更加灵活和强大的组件状态管理方式。相比于传统的 Options API,Composition API 更加注重逻辑复用和代码组织,使得复杂的组件开发变得更加清晰和可维护。

本文将深入探讨 Vue 3 Composition API 的各个方面,从基础概念到高级应用,通过实际项目案例演示如何构建复杂的组件状态管理和逻辑复用。无论你是 Vue 2 的开发者还是初学者,都能在这篇文章中找到有价值的内容。

什么是 Composition API

核心理念

Composition API 是 Vue 3 中引入的一种新的组件开发模式,它允许我们将组件的逻辑以函数的形式组织和重用。与 Options API(Vue 2 中的传统方式)不同,Composition API 不再依赖于预定义的选项(如 data、methods、computed 等),而是通过组合不同的函数来构建组件。

主要优势

  1. 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
  2. 更灵活的代码组织:按照功能而不是选项类型来组织代码
  3. 更强的类型支持:在 TypeScript 中提供更好的类型推断
  4. 更清晰的组件结构:减少重复代码,提高可读性

基础概念与核心函数

setup 函数

setup 是 Composition API 的入口函数,它在组件实例创建之前执行。在这个函数中,我们可以访问组件的所有响应式数据和方法。

import { ref, reactive } from 'vue'

export default {
  setup() {
    // 响应式数据
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    // 方法
    const increment = () => {
      count.value++
    }
    
    // 返回的数据和方法将在模板中使用
    return {
      count,
      user,
      increment
    }
  }
}

响应式数据

Composition API 提供了多种创建响应式数据的方法:

import { ref, reactive, computed, watch } from 'vue'

export default {
  setup() {
    // 创建基本响应式变量
    const count = ref(0)
    
    // 创建响应式对象
    const state = reactive({
      name: 'Vue',
      version: '3.0'
    })
    
    // 创建计算属性
    const doubleCount = computed(() => count.value * 2)
    
    // 创建监听器
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    return {
      count,
      state,
      doubleCount
    }
  }
}

基础实战:创建一个计数器组件

让我们从一个简单的计数器开始,展示如何使用 Composition API 构建基础组件。

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

<script>
import { ref } from 'vue'

export default {
  name: 'Counter',
  setup() {
    const count = ref(0)
    
    const increment = () => {
      count.value++
    }
    
    const decrement = () => {
      count.value--
    }
    
    const reset = () => {
      count.value = 0
    }
    
    return {
      count,
      increment,
      decrement,
      reset
    }
  }
}
</script>

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

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

高级特性:响应式数据管理

使用 reactive 和 ref 的区别

import { ref, reactive } from 'vue'

export default {
  setup() {
    // ref 用于基本类型和对象的包装
    const count = ref(0)
    const message = ref('Hello')
    
    // reactive 用于创建响应式对象
    const user = reactive({
      name: 'John',
      age: 30,
      address: {
        city: 'Beijing',
        country: 'China'
      }
    })
    
    // 在模板中使用时,ref 需要访问 .value
    const increment = () => {
      count.value++
    }
    
    // reactive 对象可以直接访问属性
    const updateAddress = () => {
      user.address.city = 'Shanghai'
    }
    
    return {
      count,
      message,
      user,
      increment,
      updateAddress
    }
  }
}

计算属性和监听器

<template>
  <div class="advanced-counter">
    <h2>高级计数器</h2>
    <p>原始值: {{ count }}</p>
    <p>双倍值: {{ doubleCount }}</p>
    <p>平方值: {{ squareCount }}</p>
    <p>状态: {{ status }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
  </div>
</template>

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

export default {
  setup() {
    const count = ref(0)
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2)
    const squareCount = computed(() => count.value * count.value)
    
    // 基于计算属性的状态
    const status = computed(() => {
      if (count.value < 0) return '负数'
      if (count.value === 0) return '零'
      if (count.value > 100) return '过大'
      return '正常'
    })
    
    // 监听器
    watch(count, (newVal, oldVal) => {
      console.log(`计数从 ${oldVal} 变为 ${newVal}`)
    })
    
    // 监听多个值
    watch([count], ([newCount], [oldCount]) => {
      if (newCount > 10 && oldCount <= 10) {
        console.log('超过10了!')
      }
    })
    
    const increment = () => {
      count.value++
    }
    
    const decrement = () => {
      count.value--
    }
    
    const reset = () => {
      count.value = 0
    }
    
    return {
      count,
      doubleCount,
      squareCount,
      status,
      increment,
      decrement,
      reset
    }
  }
}
</script>

逻辑复用:组合函数(Composables)

创建可复用的组合函数

// 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 double = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    double
  }
}

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

export function useLocalStorage(key, defaultValue) {
  const value = ref(defaultValue)
  
  // 初始化时从 localStorage 中读取
  const savedValue = localStorage.getItem(key)
  if (savedValue) {
    value.value = JSON.parse(savedValue)
  }
  
  // 监听值变化并保存到 localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

使用组合函数

<template>
  <div class="composable-demo">
    <h2>组合函数演示</h2>
    
    <!-- 计数器 -->
    <div class="counter-section">
      <h3>计数器</h3>
      <p>值: {{ counter.count }}</p>
      <p>双倍: {{ counter.double }}</p>
      <button @click="counter.increment">增加</button>
      <button @click="counter.decrement">减少</button>
      <button @click="counter.reset">重置</button>
    </div>
    
    <!-- 本地存储 -->
    <div class="storage-section">
      <h3>本地存储</h3>
      <input v-model="storageValue" placeholder="输入内容">
      <p>保存的值: {{ storageValue }}</p>
    </div>
  </div>
</template>

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

export default {
  setup() {
    const counter = useCounter(10)
    const storageValue = useLocalStorage('demo-value', '默认值')
    
    return {
      counter,
      storageValue
    }
  }
}
</script>

复杂组件状态管理

表单处理组合函数

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

export function useForm(initialData = {}) {
  const formData = reactive({ ...initialData })
  const errors = reactive({})
  const isSubmitting = ref(false)
  
  const validateField = (field, value) => {
    // 简单的验证规则
    if (!value && field !== 'optional') {
      errors[field] = `${field} 是必填项`
      return false
    }
    
    if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
      errors[field] = '请输入有效的邮箱地址'
      return false
    }
    
    delete errors[field]
    return true
  }
  
  const validateAll = () => {
    let isValid = true
    Object.keys(formData).forEach(field => {
      if (!validateField(field, formData[field])) {
        isValid = false
      }
    })
    return isValid
  }
  
  const setFieldValue = (field, value) => {
    formData[field] = value
    validateField(field, value)
  }
  
  const submit = async (submitHandler) => {
    if (!validateAll()) return false
    
    isSubmitting.value = true
    try {
      await submitHandler(formData)
      return true
    } catch (error) {
      console.error('提交失败:', error)
      return false
    } finally {
      isSubmitting.value = false
    }
  }
  
  const reset = () => {
    Object.keys(formData).forEach(key => {
      formData[key] = initialData[key] || ''
    })
    Object.keys(errors).forEach(key => {
      delete errors[key]
    })
  }
  
  return readonly({
    formData,
    errors,
    isSubmitting,
    setFieldValue,
    validateAll,
    submit,
    reset
  })
}

表单组件实现

<template>
  <div class="form-demo">
    <h2>表单演示</h2>
    
    <form @submit.prevent="handleSubmit">
      <div class="form-group">
        <label>姓名:</label>
        <input 
          v-model="form.formData.name" 
          type="text"
          :class="{ error: form.errors.name }"
        >
        <span v-if="form.errors.name" class="error-message">{{ form.errors.name }}</span>
      </div>
      
      <div class="form-group">
        <label>邮箱:</label>
        <input 
          v-model="form.formData.email" 
          type="email"
          :class="{ error: form.errors.email }"
        >
        <span v-if="form.errors.email" class="error-message">{{ form.errors.email }}</span>
      </div>
      
      <div class="form-group">
        <label>年龄:</label>
        <input 
          v-model.number="form.formData.age" 
          type="number"
          :class="{ error: form.errors.age }"
        >
        <span v-if="form.errors.age" class="error-message">{{ form.errors.age }}</span>
      </div>
      
      <div class="form-group">
        <label>描述:</label>
        <textarea 
          v-model="form.formData.description"
          :class="{ error: form.errors.description }"
        ></textarea>
        <span v-if="form.errors.description" class="error-message">{{ form.errors.description }}</span>
      </div>
      
      <button type="submit" :disabled="form.isSubmitting">
        {{ form.isSubmitting ? '提交中...' : '提交' }}
      </button>
    </form>
    
    <div class="result">
      <h3>当前表单数据:</h3>
      <pre>{{ JSON.stringify(form.formData, null, 2) }}</pre>
    </div>
  </div>
</template>

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

export default {
  setup() {
    const initialData = {
      name: '',
      email: '',
      age: null,
      description: ''
    }
    
    const form = useForm(initialData)
    
    const handleSubmit = async () => {
      const success = await form.submit(async (data) => {
        // 模拟异步提交
        await new Promise(resolve => setTimeout(resolve, 1000))
        console.log('表单提交成功:', data)
        alert('表单提交成功!')
      })
      
      if (!success) {
        console.log('表单验证失败')
      }
    }
    
    return {
      form,
      handleSubmit
    }
  }
}
</script>

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

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

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

input, textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
}

input.error, textarea.error {
  border-color: #ff0000;
}

.error-message {
  color: #ff0000;
  font-size: 12px;
  margin-top: 5px;
  display: block;
}

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

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.result {
  margin-top: 30px;
  padding: 15px;
  background-color: #f8f9fa;
  border-radius: 4px;
}
</style>

异步数据处理

API 请求组合函数

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

export function useApi(apiFunction) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async (...args) => {
    try {
      loading.value = true
      error.value = null
      data.value = await apiFunction(...args)
    } catch (err) {
      error.value = err.message || '请求失败'
      console.error('API 请求错误:', err)
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = null
    error.value = null
    loading.value = false
  }
  
  return readonly({
    data,
    loading,
    error,
    execute,
    reset
  })
}

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

export function usePagination(initialPage = 1, initialPageSize = 10) {
  const page = ref(initialPage)
  const pageSize = ref(initialPageSize)
  const total = ref(0)
  
  const totalPages = computed(() => {
    return Math.ceil(total.value / pageSize.value)
  })
  
  const hasPrev = computed(() => {
    return page.value > 1
  })
  
  const hasNext = computed(() => {
    return page.value < totalPages.value
  })
  
  const nextPage = () => {
    if (hasNext.value) {
      page.value++
    }
  }
  
  const prevPage = () => {
    if (hasPrev.value) {
      page.value--
    }
  }
  
  const goToPage = (newPage) => {
    if (newPage >= 1 && newPage <= totalPages.value) {
      page.value = newPage
    }
  }
  
  const setPageSize = (size) => {
    pageSize.value = size
    page.value = 1 // 重置到第一页
  }
  
  watch([page, pageSize], () => {
    // 可以在这里添加分页变化的处理逻辑
  })
  
  return {
    page,
    pageSize,
    total,
    totalPages,
    hasPrev,
    hasNext,
    nextPage,
    prevPage,
    goToPage,
    setPageSize,
    setPage: (newPage) => page.value = newPage
  }
}

数据列表组件

<template>
  <div class="data-list">
    <h2>数据列表</h2>
    
    <!-- 加载状态 -->
    <div v-if="loading" class="loading">加载中...</div>
    
    <!-- 错误处理 -->
    <div v-else-if="error" class="error">{{ error }}</div>
    
    <!-- 数据列表 -->
    <div v-else>
      <div class="pagination-controls">
        <button @click="prevPage" :disabled="!hasPrev">上一页</button>
        <span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
        <button @click="nextPage" :disabled="!hasNext">下一页</button>
      </div>
      
      <ul class="data-list-items">
        <li v-for="item in data" :key="item.id" class="data-item">
          <h3>{{ item.title }}</h3>
          <p>{{ item.content }}</p>
          <small>ID: {{ item.id }}</small>
        </li>
      </ul>
      
      <div class="pagination-controls">
        <button @click="prevPage" :disabled="!hasPrev">上一页</button>
        <span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
        <button @click="nextPage" :disabled="!hasNext">下一页</button>
      </div>
    </div>
  </div>
</template>

<script>
import { useApi } from '@/composables/useApi'
import { usePagination } from '@/composables/usePagination'

// 模拟 API 请求
const fetchItems = async (page, pageSize) => {
  // 模拟异步请求
  await new Promise(resolve => setTimeout(resolve, 1000))
  
  const start = (page - 1) * pageSize
  const items = []
  
  for (let i = 0; i < pageSize; i++) {
    items.push({
      id: start + i + 1,
      title: `项目 ${start + i + 1}`,
      content: `这是第 ${start + i + 1} 个项目的描述内容`
    })
  }
  
  return {
    items,
    total: 100
  }
}

export default {
  setup() {
    const { data, loading, error, execute } = useApi(fetchItems)
    
    const pagination = usePagination(1, 10)
    
    // 监听分页变化并重新加载数据
    const loadData = async () => {
      const result = await execute(pagination.page.value, pagination.pageSize.value)
      if (result) {
        pagination.total.value = result.total
      }
    }
    
    // 初始化加载
    loadData()
    
    // 监听分页变化
    const watchPage = () => {
      loadData()
    }
    
    // 使用 watch 监听分页参数变化
    watch([pagination.page, pagination.pageSize], watchPage)
    
    return {
      data: data.value,
      loading: loading.value,
      error: error.value,
      ...pagination
    }
  }
}
</script>

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

.loading, .error {
  text-align: center;
  padding: 20px;
}

.error {
  color: #ff0000;
  background-color: #ffebee;
  border-radius: 4px;
}

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

.data-list-items {
  list-style: none;
  padding: 0;
  margin: 0;
}

.data-item {
  border: 1px solid #eee;
  border-radius: 4px;
  padding: 15px;
  margin-bottom: 10px;
  background-color: #f9f9f9;
}

.data-item h3 {
  margin: 0 0 10px 0;
  color: #333;
}

.data-item p {
  margin: 0 0 10px 0;
  color: #666;
}

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

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

复杂状态管理:多组件通信

状态管理组合函数

// composables/useGlobalState.js
import { reactive, readonly } from 'vue'

// 全局状态存储
const globalState = reactive({
  user: null,
  theme: 'light',
  language: 'zh-CN'
})

// 状态变更函数
export function useGlobalState() {
  const setUser = (user) => {
    globalState.user = user
  }
  
  const setTheme = (theme) => {
    globalState.theme = theme
  }
  
  const setLanguage = (language) => {
    globalState.language = language
  }
  
  const logout = () => {
    globalState.user = null
  }
  
  return readonly({
    state: globalState,
    setUser,
    setTheme,
    setLanguage,
    logout
  })
}

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

export function useEventBus() {
  const events = ref({})
  
  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
  }
}

状态管理组件示例

<template>
  <div class="global-state-demo">
    <h2>全局状态管理演示</h2>
    
    <!-- 用户信息 -->
    <div class="user-info" v-if="globalState.state.user">
      <h3>用户信息</h3>
      <p>姓名: {{ globalState.state.user.name }}</p>
      <p>邮箱: {{ globalState.state.user.email }}</p>
      <button @click="logout">退出登录</button>
    </div>
    
    <!-- 登录表单 -->
    <div class="login-form" v-else>
      <h3>用户登录</h3>
      <form @submit.prevent="handleLogin">
        <input 
          v-model="loginForm.email" 
          type="email" 
          placeholder="邮箱"
          required
        >
        <input 
          v-model="loginForm.password" 
          type="password" 
          placeholder="密码"
          required
        >
        <button type="submit">登录</button>
      </form>
    </div>
    
    <!-- 主题切换 -->
    <div class="theme-controls">
      <h3>主题设置</h3>
      <button @click="setTheme('light')">浅色主题</button>
      <button @click="setTheme('dark')">深色主题</button>
      <p>当前主题: {{ globalState.state.theme }}</p>
    </div>
    
    <!-- 事件总线演示 -->
    <div class="event-bus-demo">
      <h3>事件总线演示</h3>
      <button @click="emitEvent">发送事件</button>
      <p>收到的事件: {{ eventMessage }}</p>
    </div>
  </div>
</template>

<script>
import { useGlobalState } from '@/composables/useGlobalState'
import { useEventBus } from '@/composables/useEventBus'

export default {
  setup() {
    const globalState = useGlobalState()
    const eventBus = useEventBus()
    
    const loginForm = {
      email: '',
      password: ''
    }
    
    const eventMessage = ref('')
    
    // 监听事件
    eventBus.on('demo-event', (data) => {
      eventMessage.value = `收到事件: ${JSON.stringify(data)}`
    })
    
    const handleLogin = async () => {
      // 模拟登录
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      globalState.setUser({
        name: '张三',
        email: loginForm.email
      })
      
      loginForm.email = ''
      loginForm.password = ''
    }
    
    const logout = () => {
      globalState.logout()
    }
    
    const setTheme = (theme) => {
      globalState.setTheme(theme)
    }
    
    const emitEvent = () => {
      eventBus.emit('demo-event', {
        message: 'Hello from event bus!',
        timestamp: new Date().toISOString()
      })
    }
    
    return {
      globalState,
      loginForm,
      eventMessage,
      handleLogin,
      logout,
      setTheme,
      emitEvent
    }
  }
}
</script>

<style scoped>
.global-state-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.user-info, .login-form, .theme-controls, .event-bus-demo {
  border: 1px solid #eee;
  border-radius: 4px;
  padding: 15px;
  margin-bottom: 20px;
}

form {
  display: flex;
  flex
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000