Vue3 状态管理方案对比
随着前端应用的复杂度不断提高,状态管理已成为构建可维护 Vue 应用的核心环节。Vue3 生态提供了多种状态管理解决方案,每种方案都有其适用场景和优缺点。本文将对这些主流方案进行深入对比,帮助您为项目选择最合适的状态管理策略。
为什么需要状态管理?
在深入比较各种状态管理方案前,我们需要理解为何现代前端应用需要专门的状态管理:
- 组件通信复杂度增加:当应用规模扩大,组件层级加深,仅靠 props 和事件进行通信变得繁琐
- 全局状态共享:某些状态(如用户信息、主题设置)需要在多个不相关组件间共享
- 状态变更追踪:需要集中管理状态变更,方便调试和问题定位
- 状态持久化:需要将状态保存到 localStorage 或服务端
- 更好的代码组织:将业务逻辑从视图层分离,提高可维护性
Vue3 内置的状态管理能力
1. Composition API + Reactivity API
Vue3 的响应式系统本身就提供了基础的状态管理能力:
// 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:
<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 提供了跨层级组件通信的能力,可用于简单的状态共享:
<!-- 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>
在深层嵌套的组件中使用:
<!-- 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 打造:
// 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:
// 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
}
})
在组件中使用:
<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 项目的选择:
// 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
}
})
在组件中使用:
<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请求:
// 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
}
}
在组件中使用:
<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
:
// 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
}
})
在组件中使用:
<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. 持久化策略
// 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 和优秀的开发体验,是大多数新项目的理想选择。但对于不同的项目需求,灵活选择或组合使用多种方案往往能达到更好的效果。