Vue 3 Composition API实战:构建响应式组件与状态管理完整教程

浅夏微凉
浅夏微凉 2026-01-29T15:15:14+08:00
0 0 0

前言

随着Vue.js 3的发布,开发者迎来了全新的Composition API,这一创新性的API设计彻底改变了我们编写Vue组件的方式。相比传统的Options API,Composition API提供了更灵活、更强大的组件逻辑复用能力,使得复杂应用的状态管理和组件开发变得更加直观和高效。

本文将深入探讨Vue 3 Composition API的核心概念和实际应用,从基础语法到高级特性,结合Pinia状态管理库,为读者提供一套完整的现代化Vue应用开发解决方案。无论你是Vue新手还是有经验的开发者,都能从中获得实用的知识和最佳实践。

Vue 3 Composition API核心概念

什么是Composition API

Composition API是Vue 3引入的一种新的组件逻辑组织方式。它允许我们将组件的逻辑按功能进行分组,而不是按照选项类型(如data、methods、computed等)来组织代码。这种设计使得组件更加模块化,便于维护和复用。

与Options API的区别

在Vue 2中,我们通常使用Options API来组织组件逻辑:

// Vue 2 Options API
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('')
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('Component mounted')
  }
}

而在Vue 3中,我们可以使用Composition API来实现相同的功能:

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

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

响应式基础:ref与reactive

ref的使用

ref是Vue 3中最基本的响应式API,用于创建一个响应式的引用。对于基本数据类型(字符串、数字、布尔值等),我们通常使用ref

import { ref } from 'vue'

export default {
  setup() {
    // 创建基本类型的响应式数据
    const count = ref(0)
    const name = ref('Vue')
    const isActive = ref(true)
    
    // 访问和修改值
    console.log(count.value) // 0
    count.value = 10
    console.log(count.value) // 10
    
    return {
      count,
      name,
      isActive
    }
  }
}

reactive的使用

reactive用于创建响应式的对象或数组。当我们需要管理复杂的数据结构时,通常会使用reactive

import { reactive } from 'vue'

export default {
  setup() {
    // 创建响应式对象
    const user = reactive({
      name: 'John',
      age: 30,
      email: 'john@example.com'
    })
    
    // 创建响应式数组
    const items = reactive([])
    
    // 修改数据
    user.name = 'Jane'
    items.push('item1')
    
    return {
      user,
      items
    }
  }
}

toRefs和toRef

当需要将响应式对象的属性解构为独立的ref时,可以使用toRefs

import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: 'Vue'
    })
    
    // 解构为独立的ref
    const { count, name } = toRefs(state)
    
    return {
      count,
      name
    }
  }
}

响应式计算与监听

computed计算属性

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 firstName.value.split('').reverse().join('')
      },
      set: (newValue) => {
        firstName.value = newValue.split('').reverse().join('')
      }
    })
    
    return {
      firstName,
      lastName,
      fullName,
      reversedName
    }
  }
}

watch监听器

watch用于监听响应式数据的变化:

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

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue')
    
    // 监听单个ref
    watch(count, (newValue, oldValue) => {
      console.log(`count changed from ${oldValue} to ${newValue}`)
    })
    
    // 监听多个数据源
    watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
      console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
    })
    
    // 深度监听对象
    const user = reactive({
      profile: {
        name: 'John',
        age: 30
      }
    })
    
    watch(user, (newValue, oldValue) => {
      console.log('User changed:', newValue)
    }, { deep: true })
    
    // watchEffect自动追踪依赖
    watchEffect(() => {
      console.log(`Name is now: ${name.value}`)
    })
    
    return {
      count,
      name,
      user
    }
  }
}

组件生命周期钩子

setup中的生命周期钩子

在Composition API中,我们需要通过导入相应的生命周期函数来使用它们:

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

export default {
  setup() {
    // 组件挂载时调用
    onMounted(() => {
      console.log('Component mounted')
    })
    
    // 组件更新时调用
    onUpdated(() => {
      console.log('Component updated')
    })
    
    // 组件卸载前调用
    onBeforeUnmount(() => {
      console.log('Component will unmount')
    })
    
    return {}
  }
}

在setup中使用生命周期钩子的完整示例

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

export default {
  setup() {
    const count = ref(0)
    const intervalId = ref(null)
    
    // 开始计时器
    const startTimer = () => {
      intervalId.value = setInterval(() => {
        count.value++
      }, 1000)
    }
    
    // 停止计时器
    const stopTimer = () => {
      if (intervalId.value) {
        clearInterval(intervalId.value)
        intervalId.value = null
      }
    }
    
    onMounted(() => {
      console.log('Component mounted, starting timer')
      startTimer()
    })
    
    onUnmounted(() => {
      console.log('Component unmounted, stopping timer')
      stopTimer()
    })
    
    return {
      count,
      startTimer,
      stopTimer
    }
  }
}

组件间通信与状态管理

Props和emit的使用

在Composition API中,props和emit的使用方式与Options API略有不同:

import { defineProps, defineEmits } from 'vue'

export default {
  props: {
    message: String,
    count: {
      type: Number,
      default: 0
    }
  },
  
  setup(props, { emit }) {
    // 使用props
    console.log(props.message)
    
    // 发送事件
    const handleClick = () => {
      emit('update-count', props.count + 1)
    }
    
    return {
      handleClick
    }
  }
}

使用provide和inject进行跨层级通信

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

export default {
  setup() {
    const theme = ref('dark')
    const user = ref({ name: 'John' })
    
    // 提供数据
    provide('theme', theme)
    provide('user', user)
    
    return {
      theme,
      user
    }
  }
}

// 子组件
import { inject } from 'vue'

export default {
  setup() {
    // 注入数据
    const theme = inject('theme')
    const user = inject('user')
    
    return {
      theme,
      user
    }
  }
}

Pinia状态管理库详解

Pinia基础概念

Pinia是Vue官方推荐的状态管理库,它提供了比Vuex更简洁的API和更好的TypeScript支持。Pinia的核心概念包括:

  • Store:存储应用状态的地方
  • State:应用的状态数据
  • Getters:从状态派生的数据
  • Actions:处理状态变更的方法

安装和配置Pinia

npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

创建和使用Store

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  // 状态
  state: () => ({
    name: '',
    email: '',
    isLoggedIn: false,
    profile: null
  }),
  
  // 计算属性
  getters: {
    displayName: (state) => {
      return state.name || 'Guest'
    },
    
    isPremiumUser: (state) => {
      return state.profile?.subscription === 'premium'
    }
  },
  
  // 动作
  actions: {
    login(userData) {
      this.name = userData.name
      this.email = userData.email
      this.isLoggedIn = true
      this.profile = userData.profile
    },
    
    logout() {
      this.name = ''
      this.email = ''
      this.isLoggedIn = false
      this.profile = null
    },
    
    updateProfile(updates) {
      this.profile = { ...this.profile, ...updates }
    }
  }
})

在组件中使用Store

import { useUserStore } from '@/stores/user'
import { computed } from 'vue'

export default {
  setup() {
    const userStore = useUserStore()
    
    // 直接访问状态
    const userName = computed(() => userStore.name)
    
    // 调用动作
    const handleLogin = () => {
      userStore.login({
        name: 'John Doe',
        email: 'john@example.com',
        profile: { subscription: 'premium' }
      })
    }
    
    const handleLogout = () => {
      userStore.logout()
    }
    
    return {
      userName,
      handleLogin,
      handleLogout
    }
  }
}

异步操作和中间件

// stores/api.js
import { defineStore } from 'pinia'

export const useApiStore = defineStore('api', {
  state: () => ({
    loading: false,
    error: null,
    data: null
  }),
  
  actions: {
    async fetchData(url) {
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch(url)
        const result = await response.json()
        this.data = result
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    
    // 使用中间件处理异步操作
    async fetchWithMiddleware(url, middleware) {
      this.loading = true
      
      try {
        const response = await fetch(url)
        let result = await response.json()
        
        // 应用中间件
        if (middleware && typeof middleware === 'function') {
          result = middleware(result)
        }
        
        this.data = result
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    }
  }
})

高级特性与最佳实践

组合函数的创建和复用

组合函数是Vue 3中非常重要的概念,它允许我们将可复用的逻辑封装成函数:

// 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, watch } 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)
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // 当url变化时自动重新获取数据
  watch(url, fetchData, { immediate: true })
  
  return {
    data,
    loading,
    error,
    fetchData
  }
}

在组件中使用组合函数

import { useCounter } from '@/composables/useCounter'
import { useApi } from '@/composables/useApi'

export default {
  setup() {
    // 使用计数器组合函数
    const { count, increment, decrement, doubleCount } = useCounter(10)
    
    // 使用API组合函数
    const { data, loading, error, fetchData } = useApi('/api/users')
    
    return {
      count,
      increment,
      decrement,
      doubleCount,
      data,
      loading,
      error,
      fetchData
    }
  }
}

条件渲染和动态组件

import { ref, computed } from 'vue'

export default {
  setup() {
    const currentView = ref('home')
    
    // 动态组件
    const dynamicComponent = computed(() => {
      switch (currentView.value) {
        case 'home':
          return 'HomeView'
        case 'about':
          return 'AboutView'
        case 'contact':
          return 'ContactView'
        default:
          return 'HomeView'
      }
    })
    
    // 条件渲染
    const showContent = ref(true)
    
    const toggleContent = () => {
      showContent.value = !showContent.value
    }
    
    return {
      currentView,
      dynamicComponent,
      showContent,
      toggleContent
    }
  }
}

实际项目案例:构建一个完整的待办事项应用

应用架构设计

让我们通过一个完整的待办事项应用来展示Composition API和Pinia的综合运用:

// stores/todos.js
import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todos', {
  state: () => ({
    todos: [],
    filter: 'all',
    loading: false,
    error: null
  }),
  
  getters: {
    filteredTodos: (state) => {
      switch (state.filter) {
        case 'active':
          return state.todos.filter(todo => !todo.completed)
        case 'completed':
          return state.todos.filter(todo => todo.completed)
        default:
          return state.todos
      }
    },
    
    activeCount: (state) => {
      return state.todos.filter(todo => !todo.completed).length
    },
    
    completedCount: (state) => {
      return state.todos.filter(todo => todo.completed).length
    }
  },
  
  actions: {
    async fetchTodos() {
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch('/api/todos')
        this.todos = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    
    addTodo(text) {
      const newTodo = {
        id: Date.now(),
        text,
        completed: false,
        createdAt: new Date()
      }
      
      this.todos.push(newTodo)
    },
    
    toggleTodo(id) {
      const todo = this.todos.find(todo => todo.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    
    deleteTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id)
    },
    
    clearCompleted() {
      this.todos = this.todos.filter(todo => !todo.completed)
    },
    
    setFilter(filter) {
      this.filter = filter
    }
  }
})

组件实现

<template>
  <div class="todo-app">
    <header class="todo-header">
      <h1>Todo App</h1>
    </header>
    
    <section class="todo-input">
      <input 
        v-model="newTodo" 
        @keyup.enter="addTodo"
        placeholder="What needs to be done?"
        class="todo-input-field"
      />
    </section>
    
    <section class="todo-list" v-if="!loading && !error">
      <ul>
        <li 
          v-for="todo in filteredTodos" 
          :key="todo.id"
          :class="{ completed: todo.completed }"
          class="todo-item"
        >
          <input 
            type="checkbox" 
            v-model="todo.completed"
            class="todo-checkbox"
          />
          <span class="todo-text">{{ todo.text }}</span>
          <button @click="deleteTodo(todo.id)" class="delete-btn">删除</button>
        </li>
      </ul>
    </section>
    
    <section class="todo-footer" v-if="!loading && !error">
      <div class="todo-stats">
        <span>{{ activeCount }} items left</span>
      </div>
      
      <div class="todo-filters">
        <button 
          v-for="filter in filters" 
          :key="filter"
          @click="setFilter(filter)"
          :class="{ active: filter === currentFilter }"
          class="filter-btn"
        >
          {{ filter }}
        </button>
      </div>
      
      <button 
        v-if="completedCount > 0" 
        @click="clearCompleted"
        class="clear-completed"
      >
        Clear completed
      </button>
    </section>
    
    <div v-if="loading" class="loading">Loading...</div>
    <div v-if="error" class="error">{{ error }}</div>
  </div>
</template>

<script>
import { ref, computed } from 'vue'
import { useTodoStore } from '@/stores/todos'

export default {
  name: 'TodoApp',
  
  setup() {
    const newTodo = ref('')
    const todoStore = useTodoStore()
    
    // 获取store中的数据
    const { 
      todos, 
      filter, 
      loading, 
      error,
      filteredTodos,
      activeCount,
      completedCount
    } = todoStore
    
    // 计算属性
    const currentFilter = computed(() => todoStore.filter)
    
    const filters = ['all', 'active', 'completed']
    
    // 动作方法
    const addTodo = () => {
      if (newTodo.value.trim()) {
        todoStore.addTodo(newTodo.value.trim())
        newTodo.value = ''
      }
    }
    
    const deleteTodo = (id) => {
      todoStore.deleteTodo(id)
    }
    
    const setFilter = (filter) => {
      todoStore.setFilter(filter)
    }
    
    const clearCompleted = () => {
      todoStore.clearCompleted()
    }
    
    // 初始化数据
    todoStore.fetchTodos()
    
    return {
      newTodo,
      todos,
      filter,
      loading,
      error,
      filteredTodos,
      activeCount,
      completedCount,
      currentFilter,
      filters,
      addTodo,
      deleteTodo,
      setFilter,
      clearCompleted
    }
  }
}
</script>

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

.todo-header {
  text-align: center;
  margin-bottom: 20px;
}

.todo-input {
  margin-bottom: 20px;
}

.todo-input-field {
  width: 100%;
  padding: 10px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.todo-list ul {
  list-style: none;
  padding: 0;
}

.todo-item {
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-item.completed .todo-text {
  text-decoration: line-through;
  color: #999;
}

.todo-checkbox {
  margin-right: 10px;
}

.todo-text {
  flex: 1;
}

.delete-btn {
  background: none;
  border: none;
  color: #ff4757;
  cursor: pointer;
}

.todo-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 20px;
  padding-top: 10px;
  border-top: 1px solid #eee;
}

.filter-btn {
  background: none;
  border: none;
  padding: 5px 10px;
  cursor: pointer;
  margin-right: 5px;
}

.filter-btn.active {
  font-weight: bold;
  color: #3498db;
}

.clear-completed {
  background: none;
  border: none;
  color: #ff4757;
  cursor: pointer;
}

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

.error {
  color: #ff4757;
}
</style>

性能优化和调试技巧

使用computed缓存

合理使用computed可以显著提高应用性能:

import { ref, computed } from 'vue'

export default {
  setup() {
    const items = ref([])
    
    // 对于计算量大的操作,使用computed进行缓存
    const expensiveValue = computed(() => {
      // 模拟复杂的计算
      return items.value.reduce((acc, item) => {
        // 复杂的计算逻辑
        return acc + item.value * 2
      }, 0)
    })
    
    return {
      items,
      expensiveValue
    }
  }
}

使用watch优化性能

import { ref, watch } from 'vue'

export default {
  setup() {
    const searchQuery = ref('')
    const results = ref([])
    
    // 使用防抖优化搜索
    const debouncedSearch = debounce(async (query) => {
      if (query) {
        const response = await fetch(`/api/search?q=${query}`)
        results.value = await response.json()
      } else {
        results.value = []
      }
    }, 300)
    
    watch(searchQuery, debouncedSearch)
    
    return {
      searchQuery,
      results
    }
  }
}

// 防抖函数实现
function debounce(func, wait) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}

Vue DevTools调试

在开发过程中,使用Vue DevTools可以更好地理解和调试组件状态:

import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    // 添加调试信息
    watch(count, (newVal, oldVal) => {
      console.log('Count changed:', { oldVal, newVal })
    })
    
    return {
      count
    }
  }
}

最佳实践总结

代码组织原则

  1. 按功能分组:将相关的逻辑组织在一起,避免将所有代码放在一个函数中
  2. 合理使用组合函数:将可复用的逻辑提取为组合函数
  3. 明确的数据流向:保持状态管理的清晰和一致

性能优化建议

  1. 避免不必要的计算:使用computed缓存复杂计算结果
  2. 合理使用watch:避免在watch中执行昂贵的操作
  3. 组件懒加载:对于大型应用,考虑组件的懒加载策略

开发工具推荐

  1. Vue DevTools:用于调试和性能分析
  2. ESLint:代码质量检查
  3. Prettier:代码格式化
  4. TypeScript:提供更好的类型安全

结语

Vue 3的Composition API为前端开发带来了革命性的变化,它不仅提供了更灵活的组件组织方式,还大大增强了代码的可复用性和维护性。结合Pinia状态管理库,我们能够构建出更加现代化、高效且易于维护的Vue应用。

通过本文的学习,你应该已经掌握了Composition API的核心概念和使用方法,了解了如何在实际项目中运用这些技术。记住,实践是掌握这些技能的最佳方式,建议你在自己的项目中尝试使用这些模式和最佳实践。

随着Vue生态的不断发展,我们期待看到更多基于Composition API和Pinia的优秀实践和工具出现。希望本文能够为你在Vue开发道路上提供有价值的指导和帮助。

无论你是刚刚接触Vue 3的开发者,还是想要升级现有项目的资深工程师,都建议深入学习和实践这些现代前端开发技术。相信通过不断的学习和实践,你将能够构建出更加优秀的Web应用。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000