Vue 3 Composition API最佳实践:组件复用与状态管理深度解析

晨曦吻
晨曦吻 2026-02-06T13:07:09+08:00
0 0 0

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比传统的 Options API,Composition API 提供了更灵活、更强大的组件开发方式,特别是在处理复杂逻辑和组件复用方面表现卓越。本文将深入探讨 Vue 3 Composition API 的高级用法,包括组合式函数设计、响应式数据管理、组件间通信策略等,帮助前端开发者构建更灵活、可维护的现代Web应用。

Composition API 核心概念

什么是 Composition API

Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。它允许开发者将组件的逻辑按照功能进行组合,而不是按照选项类型进行组织。这种设计模式使得代码更加模块化,易于复用和维护。

// 传统的 Options API
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  }
}

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

export default {
  setup() {
    const count = ref(0)
    const message = ref('Hello')
    
    const increment = () => {
      count.value++
    }
    
    const reversedMessage = computed(() => {
      return message.value.split('').reverse().join('')
    })
    
    return {
      count,
      message,
      increment,
      reversedMessage
    }
  }
}

setup 函数详解

setup 函数是 Composition API 的入口点,它在组件实例创建之前执行。在这个函数中,你可以访问所有 Composition API 的功能,并返回需要暴露给模板的属性和方法。

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

export default {
  setup(props, context) {
    // 接收 props
    console.log(props)
    
    // 访问 context
    console.log(context.attrs)
    console.log(context.emit)
    console.log(context.slots)
    
    // 声明响应式数据
    const count = ref(0)
    const user = reactive({
      name: 'John',
      age: 30
    })
    
    // 计算属性
    const doubledCount = computed(() => count.value * 2)
    
    // 监听器
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    // 生命周期钩子
    onMounted(() => {
      console.log('Component mounted')
    })
    
    // 返回给模板使用的数据和方法
    return {
      count,
      user,
      doubledCount,
      increment: () => count.value++
    }
  }
}

组合式函数设计模式

什么是组合式函数

组合式函数(Composable Functions)是 Vue 3 Composition API 的核心概念之一。它们是一些可复用的逻辑单元,可以封装和重用组件中的业务逻辑。

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

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

组合式函数的实际应用

<template>
  <div>
    <h2>Counter Demo</h2>
    <p>Count: {{ counter.count }}</p>
    <p>Doubled: {{ counter.doubled }}</p>
    <button @click="counter.increment">Increment</button>
    <button @click="counter.decrement">Decrement</button>
    <button @click="counter.reset">Reset</button>
    
    <h2>Fetch Demo</h2>
    <div v-if="fetcher.loading">Loading...</div>
    <div v-else-if="fetcher.error">{{ fetcher.error }}</div>
    <div v-else-if="fetcher.data">
      <pre>{{ JSON.stringify(fetcher.data, null, 2) }}</pre>
    </div>
    <button @click="fetcher.refetch">Refresh</button>
  </div>
</template>

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

export default {
  setup() {
    const counter = useCounter(10)
    const fetcher = useFetch('https://jsonplaceholder.typicode.com/posts/1')
    
    return {
      counter,
      fetcher
    }
  }
}
</script>

响应式数据管理

ref vs reactive 的深入对比

在 Vue 3 中,refreactive 是两种不同的响应式数据处理方式,它们各有适用场景。

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

// 使用 ref
const count = ref(0)
const message = ref('Hello')
const user = ref({
  name: 'John',
  age: 30
})

// 访问值时需要使用 .value
console.log(count.value) // 0
count.value++ // 增加计数

// 使用 reactive
const state = reactive({
  count: 0,
  message: 'Hello',
  user: {
    name: 'John',
    age: 30
  }
})

// 直接访问属性,无需 .value
console.log(state.count) // 0
state.count++ // 增加计数

// 使用 toRefs 转换 reactive 对象
const state = reactive({
  count: 0,
  message: 'Hello'
})

const { count, message } = toRefs(state)
// 现在 count 和 message 都是 ref,可以像普通 ref 一样使用

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

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

export default {
  setup() {
    // 响应式的数组
    const items = ref([])
    
    const addItem = (item) => {
      items.value.push(item)
    }
    
    const removeItem = (index) => {
      items.value.splice(index, 1)
    }
    
    // 响应式的对象集合
    const users = reactive(new Map())
    
    const addUser = (id, user) => {
      users.set(id, user)
    }
    
    const removeUser = (id) => {
      users.delete(id)
    }
    
    // 复杂嵌套结构的响应式处理
    const formState = reactive({
      personalInfo: {
        name: '',
        email: ''
      },
      address: {
        street: '',
        city: '',
        country: ''
      }
    })
    
    const validateForm = computed(() => {
      return formState.personalInfo.name && 
             formState.personalInfo.email &&
             formState.address.street
    })
    
    // 处理深层嵌套的响应式数据
    const deepState = reactive({
      user: {
        profile: {
          settings: {
            theme: 'light',
            notifications: true
          }
        }
      }
    })
    
    const updateTheme = (theme) => {
      deepState.user.profile.settings.theme = theme
    }
    
    return {
      items,
      addItem,
      removeItem,
      users,
      addUser,
      removeUser,
      formState,
      validateForm,
      updateTheme
    }
  }
}

响应式数据的性能优化

import { ref, reactive, computed, watchEffect, shallowRef, shallowReactive } from 'vue'

export default {
  setup() {
    // 使用 shallowRef 进行浅层响应式
    const shallowCount = shallowRef(0)
    
    // 使用 shallowReactive 进行浅层响应式对象
    const shallowState = shallowReactive({
      count: 0,
      items: []
    })
    
    // watchEffect 的使用
    const watchableData = ref(0)
    
    watchEffect(() => {
      console.log('watchEffect triggered:', watchableData.value)
      // 这个副作用会自动追踪所有被访问的响应式数据
    })
    
    // 优化计算属性
    const expensiveValue = computed({
      get: () => {
        // 复杂的计算逻辑
        return Array.from({ length: 10000 }, (_, i) => i * 2).reduce((a, b) => a + b, 0)
      },
      set: (value) => {
        // 可选的 setter
      }
    })
    
    // 带有缓存的计算属性
    const cachedComputed = computed(() => {
      return expensiveValue.value * 2
    })
    
    // 监听器优化
    const data = ref([])
    
    // 使用 watch 的 immediate 和 flush 选项
    watch(data, (newVal, oldVal) => {
      console.log('Data changed:', newVal)
    }, {
      immediate: true, // 立即执行
      flush: 'post' // 在 DOM 更新后执行
    })
    
    return {
      shallowCount,
      shallowState,
      watchableData,
      expensiveValue,
      cachedComputed,
      data
    }
  }
}

组件间通信策略

Props 和 Emit 的最佳实践

<template>
  <div class="parent-component">
    <h2>Parent Component</h2>
    <child-component 
      :user="currentUser"
      :items="listItems"
      @update-user="handleUserUpdate"
      @item-selected="handleItemSelected"
    />
    
    <p>Current User: {{ currentUser.name }}</p>
    <p>Selected Item: {{ selectedItem }}</p>
  </div>
</template>

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

export default {
  components: {
    ChildComponent
  },
  setup() {
    const currentUser = ref({
      name: 'John Doe',
      email: 'john@example.com'
    })
    
    const listItems = ref([
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' }
    ])
    
    const selectedItem = ref(null)
    
    const handleUserUpdate = (updatedUser) => {
      currentUser.value = updatedUser
    }
    
    const handleItemSelected = (item) => {
      selectedItem.value = item
    }
    
    return {
      currentUser,
      listItems,
      selectedItem,
      handleUserUpdate,
      handleItemSelected
    }
  }
}
</script>
<template>
  <div class="child-component">
    <h3>Child Component</h3>
    <div class="user-info">
      <p>Name: {{ user.name }}</p>
      <p>Email: {{ user.email }}</p>
      <button @click="updateUser">Update User</button>
    </div>
    
    <div class="items-list">
      <h4>Items:</h4>
      <ul>
        <li 
          v-for="item in items" 
          :key="item.id"
          @click="selectItem(item)"
        >
          {{ item.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    user: {
      type: Object,
      required: true
    },
    items: {
      type: Array,
      default: () => []
    }
  },
  emits: ['updateUser', 'itemSelected'],
  setup(props, { emit }) {
    const updateUser = () => {
      const updatedUser = {
        ...props.user,
        name: props.user.name + ' (Updated)'
      }
      emit('updateUser', updatedUser)
    }
    
    const selectItem = (item) => {
      emit('itemSelected', item)
    }
    
    return {
      updateUser,
      selectItem
    }
  }
}
</script>

Provide / Inject 的高级用法

// src/plugins/ThemeManager.js
import { ref, reactive } from 'vue'

export const useThemeManager = () => {
  const theme = ref('light')
  const colors = reactive({
    primary: '#007bff',
    secondary: '#6c757d',
    success: '#28a745'
  })
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  
  const setTheme = (newTheme) => {
    theme.value = newTheme
  }
  
  return {
    theme,
    colors,
    toggleTheme,
    setTheme
  }
}
<template>
  <div class="app">
    <header :class="`theme-${theme}`">
      <h1>Theme Demo</h1>
      <button @click="toggleTheme">Toggle Theme</button>
    </header>
    
    <main>
      <slot />
    </main>
  </div>
</template>

<script>
import { useThemeManager } from '@/plugins/ThemeManager'
import { provide, computed } from 'vue'

export default {
  setup() {
    const { theme, toggleTheme, colors } = useThemeManager()
    
    // 提供数据给子组件
    provide('themeContext', {
      theme,
      colors,
      toggleTheme
    })
    
    return {
      theme,
      toggleTheme
    }
  }
}
</script>
<template>
  <div class="component-with-theme">
    <h2>Component Using Theme</h2>
    <p>Current theme: {{ themeContext.theme }}</p>
    <button @click="changeColor">Change Color</button>
    
    <div 
      class="color-box" 
      :style="{ backgroundColor: themeContext.colors.primary }"
    >
      Primary Color Box
    </div>
  </div>
</template>

<script>
import { inject, computed } from 'vue'

export default {
  setup() {
    const themeContext = inject('themeContext')
    
    const changeColor = () => {
      // 模拟颜色变化
      themeContext.colors.primary = '#ff6b6b'
    }
    
    return {
      themeContext,
      changeColor
    }
  }
}
</script>

<style scoped>
.color-box {
  width: 100px;
  height: 100px;
  margin: 20px 0;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
}
</style>

状态管理与全局状态

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

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

export const useUserStore = () => {
  // 用户相关状态
  const currentUser = ref(null)
  const isLoggedIn = ref(false)
  
  // 认证状态
  const authStatus = reactive({
    loading: false,
    error: null
  })
  
  // 登录方法
  const login = async (credentials) => {
    try {
      authStatus.loading = true
      authStatus.error = null
      
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000))
      
      currentUser.value = {
        id: 1,
        name: 'John Doe',
        email: credentials.email
      }
      isLoggedIn.value = true
      
      return { success: true }
    } catch (error) {
      authStatus.error = error.message
      return { success: false, error }
    } finally {
      authStatus.loading = false
    }
  }
  
  // 登出方法
  const logout = () => {
    currentUser.value = null
    isLoggedIn.value = false
  }
  
  // 更新用户信息
  const updateUser = (updates) => {
    if (currentUser.value) {
      Object.assign(currentUser.value, updates)
    }
  }
  
  return {
    currentUser,
    isLoggedIn,
    authStatus,
    login,
    logout,
    updateUser
  }
}
<template>
  <div class="auth-demo">
    <h2>Authentication Demo</h2>
    
    <div v-if="!isLoggedIn">
      <form @submit.prevent="handleLogin">
        <input 
          v-model="loginForm.email" 
          type="email" 
          placeholder="Email"
          required
        />
        <input 
          v-model="loginForm.password" 
          type="password" 
          placeholder="Password"
          required
        />
        <button type="submit" :disabled="authStatus.loading">
          {{ authStatus.loading ? 'Logging in...' : 'Login' }}
        </button>
      </form>
      
      <div v-if="authStatus.error" class="error">
        {{ authStatus.error }}
      </div>
    </div>
    
    <div v-else>
      <p>Welcome, {{ currentUser.name }}!</p>
      <p>Email: {{ currentUser.email }}</p>
      <button @click="handleLogout">Logout</button>
      
      <button @click="updateProfile">
        Update Profile
      </button>
    </div>
  </div>
</template>

<script>
import { useUserStore } from '@/store/userStore'
import { ref } from 'vue'

export default {
  setup() {
    const { 
      currentUser, 
      isLoggedIn, 
      authStatus, 
      login, 
      logout,
      updateUser
    } = useUserStore()
    
    const loginForm = ref({
      email: '',
      password: ''
    })
    
    const handleLogin = async () => {
      const result = await login(loginForm.value)
      if (result.success) {
        loginForm.value = { email: '', password: '' }
      }
    }
    
    const handleLogout = () => {
      logout()
    }
    
    const updateProfile = () => {
      updateUser({
        name: 'John Smith'
      })
    }
    
    return {
      currentUser,
      isLoggedIn,
      authStatus,
      loginForm,
      handleLogin,
      handleLogout,
      updateProfile
    }
  }
}
</script>

复杂状态管理的组合式函数实现

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

export function useAsyncState(promiseFn, initialValue = null) {
  const data = ref(initialValue)
  const loading = ref(false)
  const error = ref(null)
  const executed = ref(false)
  
  const execute = async (...args) => {
    try {
      loading.value = true
      error.value = null
      data.value = await promiseFn(...args)
      executed.value = true
    } catch (err) {
      error.value = err
      data.value = initialValue
    } finally {
      loading.value = false
    }
  }
  
  const reset = () => {
    data.value = initialValue
    error.value = null
    executed.value = false
  }
  
  return {
    data,
    loading,
    error,
    executed,
    execute,
    reset
  }
}

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

export function usePagination(data, pageSize = 10) {
  const currentPage = ref(1)
  const _pageSize = ref(pageSize)
  
  const totalPages = computed(() => {
    return Math.ceil(data.value.length / _pageSize.value)
  })
  
  const paginatedData = computed(() => {
    const start = (currentPage.value - 1) * _pageSize.value
    const end = start + _pageSize.value
    return data.value.slice(start, end)
  })
  
  const hasNextPage = computed(() => {
    return currentPage.value < totalPages.value
  })
  
  const hasPrevPage = computed(() => {
    return currentPage.value > 1
  })
  
  const nextPage = () => {
    if (hasNextPage.value) {
      currentPage.value++
    }
  }
  
  const prevPage = () => {
    if (hasPrevPage.value) {
      currentPage.value--
    }
  }
  
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  const setPageSize = (size) => {
    _pageSize.value = size
    currentPage.value = 1
  }
  
  return {
    currentPage,
    totalPages,
    paginatedData,
    hasNextPage,
    hasPrevPage,
    nextPage,
    prevPage,
    goToPage,
    setPageSize
  }
}
<template>
  <div class="advanced-demo">
    <h2>Advanced State Management Demo</h2>
    
    <!-- 异步状态管理 -->
    <div class="async-section">
      <h3>Async Data Fetching</h3>
      <button @click="fetchPosts">Fetch Posts</button>
      <div v-if="posts.loading">Loading...</div>
      <div v-else-if="posts.error" class="error">
        {{ posts.error }}
      </div>
      <div v-else-if="posts.data">
        <ul>
          <li v-for="post in posts.data" :key="post.id">
            {{ post.title }}
          </li>
        </ul>
      </div>
    </div>
    
    <!-- 分页管理 -->
    <div class="pagination-section">
      <h3>Pagination Demo</h3>
      <div class="pagination-controls">
        <button @click="goToPage(1)" :disabled="currentPage === 1">
          First
        </button>
        <button @click="prevPage" :disabled="!hasPrevPage">
          Previous
        </button>
        <span>Page {{ currentPage }} of {{ totalPages }}</span>
        <button @click="nextPage" :disabled="!hasNextPage">
          Next
        </button>
        <button @click="goToPage(totalPages)" :disabled="currentPage === totalPages">
          Last
        </button>
      </div>
      
      <div class="paginated-list">
        <div 
          v-for="item in paginatedData" 
          :key="item.id"
          class="list-item"
        >
          {{ item.name }}
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { useAsyncState } from '@/composables/useAsyncState'
import { usePagination } from '@/composables/usePagination'
import { ref, computed } from 'vue'

export default {
  setup() {
    // 异步状态管理
    const fetchPosts = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
        const data = await response.json()
        posts.data = data
      } catch (error) {
        posts.error = error.message
      }
    }
    
    const posts = useAsyncState(fetchPosts, [])
    
    // 分页管理
    const items = ref([
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' },
      { id: 4, name: 'Item 4' },
      { id: 5, name: 'Item 5' },
      { id: 6, name: 'Item 6' },
      { id: 7, name: 'Item 7' },
      { id: 8, name: 'Item 8' },
      { id: 9, name: 'Item 9' },
      { id: 10, name: 'Item 10' }
    ])
    
    const {
      currentPage,
      totalPages,
      paginatedData,
      hasNextPage,
      hasPrevPage,
      nextPage,
      prevPage,
      goToPage
    } = usePagination(items, 3)
    
    return {
      posts,
      currentPage,
      totalPages,
      paginatedData,
      hasNextPage,
      hasPrevPage,
      nextPage,
      prevPage,
      goToPage,
      fetchPosts
    }
  }
}
</script>

<style scoped>
.async-section, .pagination-section {
  margin: 20px 0;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.pagination-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 15px;
}

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

.error {
  color: red;
  font-weight: bold;
}
</style>

性能优化与最佳实践

组件复用的最佳实践

<template>
  <div class="reusable-component">
    <h2>{{ title }}</h2>
    <div class="content">
      <slot name="header"></slot>
      <p>{{ content }}</p>
      <slot></slot>
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: 'Default Title'
    },
    content: {
      type: String,
      default: ''
    }
  }
}
</script>

<style scoped>
.reusable-component {
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 15px;
  margin: 10px 0;
}

.content {
  margin-top: 10px;
}
</style>
<template>
  <div class="complex-demo">
    <h2>Reusable Component Demo</h2>
    
    <reusable-component 
      title="User Profile" 
      content="This is a user profile component"
    >
      <template #header>
        <div class="profile-header">
          <img :src="user.avatar" alt="Avatar" />
          <h3>{{ user.name }}</h3>
        </div>
      </template>
      
      <div class="profile-details">
        <p>Email: {{ user.email }}</p>
        <p>Role: {{ user.role }}</p>
      </div>
      
      <template #footer>
        <button @click="editProfile">Edit Profile</button>
      </template>
    </reusable-component>
  </div>
</template>

<script>
import ReusableComponent from './ReusableComponent.vue'
import { ref }
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000