Skip to content

Vue3 状态管理方案对比

随着前端应用的复杂度不断提高,状态管理已成为构建可维护 Vue 应用的核心环节。Vue3 生态提供了多种状态管理解决方案,每种方案都有其适用场景和优缺点。本文将对这些主流方案进行深入对比,帮助您为项目选择最合适的状态管理策略。

为什么需要状态管理?

在深入比较各种状态管理方案前,我们需要理解为何现代前端应用需要专门的状态管理:

  1. 组件通信复杂度增加:当应用规模扩大,组件层级加深,仅靠 props 和事件进行通信变得繁琐
  2. 全局状态共享:某些状态(如用户信息、主题设置)需要在多个不相关组件间共享
  3. 状态变更追踪:需要集中管理状态变更,方便调试和问题定位
  4. 状态持久化:需要将状态保存到 localStorage 或服务端
  5. 更好的代码组织:将业务逻辑从视图层分离,提高可维护性

Vue3 内置的状态管理能力

1. Composition API + Reactivity API

Vue3 的响应式系统本身就提供了基础的状态管理能力:

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

// 创建响应式状态
const state = reactive({
  count: 0,
  todos: []
})

// 定义操作状态的方法
const actions = {
  increment() {
    state.count++
  },
  addTodo(text) {
    state.todos.push({
      id: Date.now(),
      text,
      completed: false
    })
  },
  toggleTodo(id) {
    const todo = state.todos.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
}

// 导出只读状态和actions
export default {
  state: readonly(state),
  ...actions
}

使用这个简单的 store:

vue
<script setup>
import store from './store.js'
import { computed } from 'vue'

const { state, increment, addTodo, toggleTodo } = store
const completedCount = computed(() => {
  return state.todos.filter(todo => todo.completed).length
})

function handleAddTodo() {
  addTodo('新任务')
}
</script>

<template>
  <div>
    <p>计数: {{ state.count }}</p>
    <button @click="increment">增加</button>
    
    <div>
      <p>任务完成: {{ completedCount }} / {{ state.todos.length }}</p>
      <button @click="handleAddTodo">添加任务</button>
      <ul>
        <li v-for="todo in state.todos" :key="todo.id">
          <input 
            type="checkbox" 
            :checked="todo.completed" 
            @change="toggleTodo(todo.id)" 
          />
          {{ todo.text }}
        </li>
      </ul>
    </div>
  </div>
</template>

优点

  • 零依赖,不需要引入额外库
  • 轻量简洁,适合小型应用
  • 充分利用 Vue3 响应式系统
  • 良好的 TypeScript 支持

缺点

  • 缺乏规范的状态管理模式
  • 没有内置的调试工具
  • 大型应用中可能导致代码组织混乱
  • 跨组件状态共享需要自行实现

2. provide/inject

Vue3 的 provide/inject API 提供了跨层级组件通信的能力,可用于简单的状态共享:

vue
<!-- App.vue -->
<script setup>
import { provide, reactive } from 'vue'

const state = reactive({
  theme: 'light',
  user: null
})

function toggleTheme() {
  state.theme = state.theme === 'light' ? 'dark' : 'light'
}

function login(userData) {
  state.user = userData
}

function logout() {
  state.user = null
}

// 提供状态和方法给后代组件
provide('appState', state)
provide('toggleTheme', toggleTheme)
provide('login', login)
provide('logout', logout)
</script>

在深层嵌套的组件中使用:

vue
<!-- DeepChildComponent.vue -->
<script setup>
import { inject } from 'vue'

const appState = inject('appState')
const toggleTheme = inject('toggleTheme')
const logout = inject('logout')
</script>

<template>
  <div :class="appState.theme">
    <button @click="toggleTheme">
      切换到{{ appState.theme === 'light' ? '深色' : '浅色' }}模式
    </button>
    
    <div v-if="appState.user">
      <p>欢迎, {{ appState.user.name }}</p>
      <button @click="logout">退出登录</button>
    </div>
  </div>
</template>

优点

  • Vue 内置功能,无需额外依赖
  • 解决了深层组件通信问题
  • 可与 Composition API 完美结合

缺点

  • 状态来源不明确,代码可读性下降
  • 大型应用中难以维护
  • 状态变更难以追踪
  • 缺乏严格的架构约束

专业状态管理方案

1. Pinia

Pinia 是 Vue 官方推荐的状态管理库,被设计为 Vuex 的继任者,专为 Vue3 打造:

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

// 定义store
export const useCounterStore = defineStore('counter', {
  // 状态
  state: () => ({
    count: 0,
    lastChanged: null
  }),
  
  // 计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    isPositive: (state) => state.count > 0
  },
  
  // 操作方法
  actions: {
    increment() {
      this.count++
      this.lastChanged = new Date()
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    }
  }
})

组合式API风格的Pinia store:

js
// stores/todo.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useTodoStore = defineStore('todo', () => {
  // 状态
  const todos = ref([])
  const filter = ref('all')
  
  // getters
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'completed':
        return todos.value.filter(todo => todo.completed)
      case 'active':
        return todos.value.filter(todo => !todo.completed)
      default:
        return todos.value
    }
  })
  
  // actions
  function addTodo(text) {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false
    })
  }
  
  function toggleTodo(id) {
    const todo = todos.value.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  return {
    todos,
    filter,
    filteredTodos,
    addTodo,
    toggleTodo,
    setFilter
  }
})

在组件中使用:

vue
<script setup>
import { useCounterStore } from '@/stores/counter'
import { useTodoStore } from '@/stores/todo'
import { storeToRefs } from 'pinia'

// 获取store实例
const counterStore = useCounterStore()
const todoStore = useTodoStore()

// 解构store,保持响应性
const { count, doubleCount } = storeToRefs(counterStore)
const { filteredTodos, filter } = storeToRefs(todoStore)

// 直接使用actions
function handleAddTodo() {
  todoStore.addTodo('新任务')
}
</script>

<template>
  <div>
    <h2>计数器</h2>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="counterStore.increment">增加</button>
    <button @click="counterStore.incrementAsync">异步增加</button>
    
    <h2>任务列表</h2>
    <div>
      <button @click="todoStore.setFilter('all')">全部</button>
      <button @click="todoStore.setFilter('active')">未完成</button>
      <button @click="todoStore.setFilter('completed')">已完成</button>
      <p>当前筛选: {{ filter }}</p>
    </div>
    
    <button @click="handleAddTodo">添加任务</button>
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.completed" 
          @change="todoStore.toggleTodo(todo.id)" 
        />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

优点

  • Vue 官方推荐的解决方案
  • 支持两种定义 store 的风格(Options API 和 Composition API)
  • 优秀的 TypeScript 支持
  • 内置 devtools 集成
  • 自动代码拆分,按需加载 store
  • 轻量级(约 1KB)
  • 简单直观的 API,学习成本低
  • 支持插件扩展(持久化、缓存等)

缺点

  • 相比 Vuex,缺少一些固定的架构约束
  • 仍处于发展阶段,生态不如 Vuex 成熟

2. Vuex 4

虽然 Pinia 已成为官方推荐,但 Vuex 4 仍然是许多 Vue3 项目的选择:

js
// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state() {
    return {
      count: 0,
      todos: []
    }
  },
  
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
    completedTodos(state) {
      return state.todos.filter(todo => todo.completed)
    }
  },
  
  mutations: {
    increment(state) {
      state.count++
    },
    addTodo(state, text) {
      state.todos.push({
        id: Date.now(),
        text,
        completed: false
      })
    },
    toggleTodo(state, id) {
      const todo = state.todos.find(todo => todo.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    }
  },
  
  actions: {
    incrementAsync({ commit }) {
      return new Promise(resolve => {
        setTimeout(() => {
          commit('increment')
          resolve()
        }, 1000)
      })
    },
    async fetchTodos({ commit }) {
      try {
        const response = await fetch('/api/todos')
        const todos = await response.json()
        todos.forEach(todo => {
          commit('addTodo', todo.text)
        })
      } catch (error) {
        console.error('Failed to fetch todos:', error)
      }
    }
  },
  
  modules: {
    // 可添加模块化的store
  }
})

在组件中使用:

vue
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()

// 计算属性获取状态
const count = computed(() => store.state.count)
const doubleCount = computed(() => store.getters.doubleCount)
const todos = computed(() => store.state.todos)
const completedTodos = computed(() => store.getters.completedTodos)

// 方法
function addTodo() {
  store.commit('addTodo', '新任务')
}

function toggleTodo(id) {
  store.commit('toggleTodo', id)
}
</script>

<template>
  <div>
    <h2>计数器</h2>
    <p>计数: {{ count }}</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="store.commit('increment')">增加</button>
    <button @click="store.dispatch('incrementAsync')">异步增加</button>
    
    <h2>任务列表</h2>
    <button @click="addTodo">添加任务</button>
    
    <div>
      <p>完成: {{ completedTodos.length }} / {{ todos.length }}</p>
      <button @click="store.dispatch('fetchTodos')">加载任务</button>
    </div>
    
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.completed" 
          @change="toggleTodo(todo.id)" 
        />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

优点

  • 成熟稳定,有大量生产实践
  • 严格的架构约束(state, mutations, actions, getters)
  • 丰富的插件生态
  • 内置 devtools 集成
  • 支持动态模块注册

缺点

  • TypeScript 支持相对较弱
  • API 相对冗长
  • mutation/action 分离导致代码重复
  • 与 Composition API 结合不够自然

3. Vue Query(TanStack Query)

TanStack Query (Vue Query) 是一个专注于服务端状态管理的库,特别适合处理API请求:

js
// composables/usePosts.js
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'

const fetchPosts = async () => {
  const response = await fetch('/api/posts')
  if (!response.ok) {
    throw new Error('Network response was not ok')
  }
  return response.json()
}

const createPost = async (newPost) => {
  const response = await fetch('/api/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newPost),
  })
  if (!response.ok) {
    throw new Error('Failed to create post')
  }
  return response.json()
}

export function usePosts() {
  const queryClient = useQueryClient()
  
  // 获取文章列表
  const postsQuery = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 60 * 1000, // 1分钟内不重新获取
  })
  
  // 创建文章
  const createPostMutation = useMutation({
    mutationFn: createPost,
    onSuccess: () => {
      // 创建成功后,使缓存失效,触发重新获取
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })
  
  return {
    posts: postsQuery.data,
    isLoading: postsQuery.isLoading,
    isError: postsQuery.isError,
    error: postsQuery.error,
    createPost: createPostMutation.mutate,
    isCreating: createPostMutation.isPending
  }
}

在组件中使用:

vue
<script setup>
import { ref } from 'vue'
import { usePosts } from '@/composables/usePosts'

const { 
  posts, 
  isLoading, 
  isError, 
  error, 
  createPost, 
  isCreating 
} = usePosts()

const newPostTitle = ref('')
const newPostContent = ref('')

function handleSubmit() {
  if (newPostTitle.value && newPostContent.value) {
    createPost({
      title: newPostTitle.value,
      content: newPostContent.value
    })
    newPostTitle.value = ''
    newPostContent.value = ''
  }
}
</script>

<template>
  <div>
    <h1>博客文章</h1>
    
    <!-- 数据加载状态 -->
    <div v-if="isLoading">加载中...</div>
    <div v-else-if="isError">加载失败: {{ error.message }}</div>
    
    <!-- 文章列表 -->
    <div v-else-if="posts">
      <article v-for="post in posts" :key="post.id">
        <h2>{{ post.title }}</h2>
        <p>{{ post.content }}</p>
      </article>
    </div>
    
    <!-- 创建新文章 -->
    <form @submit.prevent="handleSubmit">
      <h2>发布新文章</h2>
      <div>
        <label for="title">标题:</label>
        <input id="title" v-model="newPostTitle" required />
      </div>
      <div>
        <label for="content">内容:</label>
        <textarea id="content" v-model="newPostContent" required></textarea>
      </div>
      <button type="submit" :disabled="isCreating">
        {{ isCreating ? '发布中...' : '发布文章' }}
      </button>
    </form>
  </div>
</template>

优点

  • 专为API请求设计,内置缓存和失效策略
  • 优化数据获取,减少不必要的请求
  • 内置加载、错误状态管理
  • 自动重试和背景刷新
  • 去抖动和请求合并
  • 支持分页和无限滚动
  • 可与 Pinia 或 Vuex 结合使用

缺点

  • 仅专注于服务端状态管理,客户端状态需其他解决方案
  • 学习曲线相对陡峭
  • Vue 生态系统中相对较新

4. Vueuse/core 的 createGlobalState

VueUse 提供了一种轻量级的状态共享方案 - createGlobalState

js
// stores/useGlobalState.js
import { createGlobalState, useStorage } from '@vueuse/core'
import { ref, computed } from 'vue'

export const useGlobalState = createGlobalState(() => {
  // 使用 localStorage 持久化计数器
  const count = useStorage('app-count', 0)
  
  // 普通状态
  const todos = ref([])
  const filter = ref('all')
  
  // 计算属性
  const doubleCount = computed(() => count.value * 2)
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case 'completed':
        return todos.value.filter(todo => todo.completed)
      case 'active':
        return todos.value.filter(todo => !todo.completed)
      default:
        return todos.value
    }
  })
  
  // 方法
  function increment() {
    count.value++
  }
  
  function addTodo(text) {
    todos.value.push({
      id: Date.now(),
      text,
      completed: false
    })
  }
  
  function toggleTodo(id) {
    const todo = todos.value.find(todo => todo.id === id)
    if (todo) {
      todo.completed = !todo.completed
    }
  }
  
  function setFilter(newFilter) {
    filter.value = newFilter
  }
  
  // 将所有内容返回
  return {
    count,
    todos,
    filter,
    doubleCount,
    filteredTodos,
    increment,
    addTodo,
    toggleTodo,
    setFilter
  }
})

在组件中使用:

vue
<script setup>
import { useGlobalState } from '@/stores/useGlobalState'

const {
  count,
  doubleCount,
  todos,
  filteredTodos,
  filter,
  increment,
  addTodo,
  toggleTodo,
  setFilter
} = useGlobalState()
</script>

<template>
  <div>
    <h2>计数器</h2>
    <p>计数: {{ count }} (保存在localStorage)</p>
    <p>双倍: {{ doubleCount }}</p>
    <button @click="increment">增加</button>
    
    <h2>任务列表</h2>
    <div>
      <button @click="setFilter('all')">全部</button>
      <button @click="setFilter('active')">未完成</button>
      <button @click="setFilter('completed')">已完成</button>
      <p>当前筛选: {{ filter }}</p>
    </div>
    
    <button @click="addTodo('新任务')">添加任务</button>
    
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id">
        <input 
          type="checkbox" 
          :checked="todo.completed" 
          @change="toggleTodo(todo.id)" 
        />
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

优点

  • 极其轻量
  • 使用简单直观
  • 集成VueUse的其他工具(如useStorage实现持久化)
  • 与Composition API完美融合

缺点

  • 缺少约束和规范
  • 没有专门的调试工具
  • 对于大型应用可能不够严谨
  • 缺少内置的高级特性(如时间旅行调试)

方案选择建议

根据项目规模和需求选择适合的方案:

小型项目/原型开发

  • 推荐:Composition API + Reactivity API 或 VueUse
  • 理由:设置简单,无额外依赖,学习成本低

中型项目

  • 推荐:Pinia
  • 理由:轻量但功能完整,良好的开发体验,官方支持

大型/企业级项目

  • 推荐:Pinia + Vue Query 的组合
  • 理由
    • Pinia 管理客户端状态
    • Vue Query 处理所有API请求相关状态
    • 结合了两者的优势,同时保持代码组织清晰

迁移中的项目

  • 推荐:Vuex 4
  • 理由:如果已有大量 Vuex 代码,继续使用 Vuex 4 减少迁移成本

最佳实践

无论选择哪种方案,以下最佳实践都值得参考:

1. 合理拆分状态

  • 避免所有状态放在一个巨大的 store 中
  • 根据功能或领域拆分 store
  • 仅全局共享的状态才放入全局 store

2. 考虑开发体验

  • 添加适当的类型定义
  • 利用开发工具(如Vue Devtools)
  • 使用描述性的变量和函数名

3. 注意性能

  • 避免过度响应式,合理使用 shallowRef/shallowReactive
  • 对大型集合考虑虚拟化或分页
  • 预加载可能需要的数据

4. 持久化策略

js
// Pinia 持久化示例
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// 在store中配置
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    preferences: {}
  }),
  actions: {
    // ...
  },
  // 持久化配置
  persist: {
    key: 'user-store',
    storage: localStorage,
    paths: ['preferences'] // 只持久化preferences
  }
})

总结

Vue3 提供了丰富的状态管理解决方案,适合不同规模和需求的项目:

  • 小型项目:Composition API + Reactivity 或 VueUse 的 createGlobalState
  • 中型项目:Pinia
  • 大型项目:Pinia + Vue Query
  • 迁移项目:Vuex 4

选择状态管理方案时,要考虑项目规模、团队熟悉度、学习成本和长期维护等因素。随着应用的发展,状态管理策略也应适时调整,保持代码的可维护性和性能。

在 Vue3 的生态系统中,Pinia 已成为官方推荐的状态管理解决方案,结合其简洁的 API 和优秀的开发体验,是大多数新项目的理想选择。但对于不同的项目需求,灵活选择或组合使用多种方案往往能达到更好的效果。