什么是状态管理?
在 Vue 开发中,状态管理是一个核心概念。简单来说,状态就是驱动应用的数据源。每一个 Vue 组件实例都在管理自己的响应式状态,让我们从一个简单的计数器组件开始理解:
1<script setup> 2import { ref } from 'vue' 3 4// 状态 - 驱动应用的数据源 5const count = ref(0) 6 7// 动作 - 修改状态的方法 8function increment() { 9 count.value++ 10} 11</script> 12 13<!-- 视图 - 状态的声明式映射 --> 14<template>{{ count }}</template> 15
这个简单的例子展示了状态管理的三个核心要素:
- 状态:数据源 (
count) - 视图:状态的声明式映射 (模板)
- 动作:状态变更的逻辑 (
increment)
这就是所谓的"单向数据流"概念。
为什么需要状态管理?
当应用变得复杂时,我们会遇到两个典型问题:
问题 1:多个组件共享状态
1<!-- ComponentA.vue --> 2<template>组件 A: {{ count }}</template> 3 4<!-- ComponentB.vue --> 5<template>组件 B: {{ count }}</template> 6
如果多个视图依赖于同一份状态,传统的解决方案是通过 props 逐级传递,但这在深层次组件树中会变得非常繁琐,导致 Prop 逐级透传问题。
问题 2:多组件修改同一状态
来自不同视图的交互都需要更改同一份状态时,直接通过事件或模板引用会导致代码难以维护。
解决方案:将共享状态抽取到全局单例中管理。
使用响应式 API 实现简单状态管理
Vue 的响应式系统本身就提供了状态管理的能力。
创建全局状态 Store
1// store.js 2import { reactive } from 'vue' 3 4export const store = reactive({ 5 count: 0, 6 user: null, 7 todos: [] 8}) 9
在组件中使用
1<!-- ComponentA.vue --> 2<script setup> 3import { store } from './store.js' 4</script> 5 6<template> 7 <div>From A: {{ store.count }}</div> 8 <button @click="store.count++">+1</button> 9</template> 10
1<!-- ComponentB.vue --> 2<script setup> 3import { store } from './store.js' 4</script> 5 6<template> 7 <div>From B: {{ store.count }}</div> 8 <button @click="store.count++">+1</button> 9</template> 10
问题:任意修改的风险
上面的实现有个问题:任何导入 store 的组件都可以随意修改状态,这在大型应用中难以维护。
改进:封装状态修改逻辑
1// store.js 2import { reactive } from 'vue' 3 4export const store = reactive({ 5 // 状态 6 count: 0, 7 user: null, 8 todos: [], 9 10 // 动作 - 封装状态修改逻辑 11 increment() { 12 this.count++ 13 }, 14 15 setUser(user) { 16 this.user = user 17 }, 18 19 addTodo(todo) { 20 this.todos.push(todo) 21 }, 22 23 removeTodo(id) { 24 this.todos = this.todos.filter(todo => todo.id !== id) 25 } 26}) 27
1<!-- ComponentB.vue --> 2<script setup> 3import { store } from './store.js' 4</script> 5 6<template> 7 <button @click="store.increment()"> 8 From B: {{ store.count }} 9 </button> 10</template> 11
注意:这里使用 store.increment() 带圆括号调用,因为它不是组件方法,需要正确的 this 上下文。
使用组合式函数管理状态
1// useCounter.js 2import { ref } from 'vue' 3 4// 全局状态 5const globalCount = ref(1) 6 7export function useCount() { 8 // 局部状态 9 const localCount = ref(1) 10 11 function incrementGlobal() { 12 globalCount.value++ 13 } 14 15 function incrementLocal() { 16 localCount.value++ 17 } 18 19 return { 20 globalCount: readonly(globalCount), // 使用 readonly 保护全局状态 21 localCount, 22 incrementGlobal, 23 incrementLocal 24 } 25} 26
Pinia:现代化的状态管理库
虽然手动状态管理在简单场景中足够,但生产级应用需要更多功能:
- 团队协作约定
- Vue DevTools 集成
- 模块热更新
- 服务端渲染支持
- 完善的 TypeScript 支持
Pinia 是 Vue 官方推荐的状态管理库,它解决了上述所有问题。
为什么选择 Pinia?
- ✅ 类型安全:完美的 TypeScript 支持
- ✅ DevTools 支持:时间旅行调试等
- ✅ 模块热更新:开发时保持状态
- ✅ 简洁的 API:学习成本低
- ✅ 组合式 API:与 Vue 3 完美契合
安装和配置
1npm install pinia 2
1// main.js 2import { createApp } from 'vue' 3import { createPinia } from 'pinia' 4import App from './App.vue' 5 6const pinia = createPinia() 7const app = createApp(App) 8 9app.use(pinia) 10app.mount('#app') 11
创建 Store
选项式 Store
1// stores/counter.js 2import { defineStore } from 'pinia' 3 4export const useCounterStore = defineStore('counter', { 5 // 状态 6 state: () => ({ 7 count: 0, 8 user: null 9 }), 10 11 // 计算属性 12 getters: { 13 doubleCount: (state) => state.count * 2, 14 isAuthenticated: (state) => state.user !== null 15 }, 16 17 // 动作 18 actions: { 19 increment() { 20 this.count++ 21 }, 22 async login(credentials) { 23 const user = await api.login(credentials) 24 this.user = user 25 }, 26 logout() { 27 this.user = null 28 } 29 } 30}) 31
组合式 Store(推荐)
1// stores/counter.js 2import { defineStore } from 'pinia' 3import { ref, computed } from 'vue' 4 5export const useCounterStore = defineStore('counter', () => { 6 // 状态 7 const count = ref(0) 8 const user = ref(null) 9 10 // 计算属性 11 const doubleCount = computed(() => count.value * 2) 12 const isAuthenticated = computed(() => user.value !== null) 13 14 // 动作 15 function increment() { 16 count.value++ 17 } 18 19 async function login(credentials) { 20 const response = await fetch('/api/login', { 21 method: 'POST', 22 body: JSON.stringify(credentials) 23 }) 24 user.value = await response.json() 25 } 26 27 function logout() { 28 user.value = null 29 } 30 31 return { 32 count, 33 user, 34 doubleCount, 35 isAuthenticated, 36 increment, 37 login, 38 logout 39 } 40}) 41
在组件中使用 Store
1<script setup> 2import { useCounterStore } from '@/stores/counter' 3import { storeToRefs } from 'pinia' 4 5const counterStore = useCounterStore() 6 7// 使用 storeToRefs 保持响应式并解构 8const { count, doubleCount, isAuthenticated } = storeToRefs(counterStore) 9const { increment, login } = counterStore 10 11// 直接修改状态(不推荐) 12const directIncrement = () => { 13 counterStore.count++ 14} 15 16// 使用 action(推荐) 17const actionIncrement = () => { 18 counterStore.increment() 19} 20 21// 批量修改 22const patchUpdate = () => { 23 counterStore.$patch({ 24 count: counterStore.count + 1, 25 user: { name: 'Updated User' } 26 }) 27} 28 29// 重置状态 30const resetStore = () => { 31 counterStore.$reset() 32} 33 34// 订阅状态变化 35counterStore.$subscribe((mutation, state) => { 36 console.log('状态变化:', mutation) 37 console.log('新状态:', state) 38}) 39</script> 40 41<template> 42 <div> 43 <p>计数: {{ count }}</p> 44 <p>双倍计数: {{ doubleCount }}</p> 45 <p>认证状态: {{ isAuthenticated ? '已登录' : '未登录' }}</p> 46 47 <button @click="increment">增加</button> 48 <button @click="directIncrement">直接增加</button> 49 <button @click="patchUpdate">批量更新</button> 50 <button @click="resetStore">重置</button> 51 </div> 52</template> 53
在 Store 之间使用其他 Store
1// stores/auth.js 2import { defineStore } from 'pinia' 3 4export const useAuthStore = defineStore('auth', () => { 5 const user = ref(null) 6 const token = ref('') 7 8 function setAuth(userData, authToken) { 9 user.value = userData 10 token.value = authToken 11 } 12 13 return { user, token, setAuth } 14}) 15 16// stores/todos.js 17import { defineStore } from 'pinia' 18import { useAuthStore } from './auth' 19 20export const useTodosStore = defineStore('todos', () => { 21 const authStore = useAuthStore() 22 const todos = ref([]) 23 24 async function fetchTodos() { 25 // 使用其他 store 的状态 26 if (!authStore.token) { 27 throw new Error('未认证') 28 } 29 30 const response = await fetch('/api/todos', { 31 headers: { 32 Authorization: `Bearer ${authStore.token}` 33 } 34 }) 35 todos.value = await response.json() 36 } 37 38 return { todos, fetchTodos } 39}) 40
高级模式和最佳实践
1. 数据持久化
1// plugins/persistence.js 2import { watch } from 'vue' 3 4export function persistStore(store, key = store.$id) { 5 // 从 localStorage 恢复状态 6 const persisted = localStorage.getItem(key) 7 if (persisted) { 8 store.$patch(JSON.parse(persisted)) 9 } 10 11 // 监听状态变化并保存 12 watch( 13 () => store.$state, 14 (state) => { 15 localStorage.setItem(key, JSON.stringify(state)) 16 }, 17 { deep: true } 18 ) 19} 20 21// 在 store 中使用 22export const usePersistedStore = defineStore('persisted', () => { 23 const state = ref({}) 24 25 // 在 store 创建后调用 26 onMounted(() => { 27 persistStore(usePersistedStore()) 28 }) 29 30 return { state } 31}) 32
2. API 集成模式
1// stores/posts.js 2import { defineStore } from 'pinia' 3 4export const usePostsStore = defineStore('posts', () => { 5 const posts = ref([]) 6 const loading = ref(false) 7 const error = ref(null) 8 9 async function fetchPosts() { 10 loading.value = true 11 error.value = null 12 13 try { 14 const response = await fetch('/api/posts') 15 if (!response.ok) throw new Error('获取失败') 16 posts.value = await response.json() 17 } catch (err) { 18 error.value = err.message 19 } finally { 20 loading.value = false 21 } 22 } 23 24 async function createPost(postData) { 25 const response = await fetch('/api/posts', { 26 method: 'POST', 27 body: JSON.stringify(postData) 28 }) 29 const newPost = await response.json() 30 posts.value.push(newPost) 31 return newPost 32 } 33 34 return { 35 posts, 36 loading, 37 error, 38 fetchPosts, 39 createPost 40 } 41}) 42
3. 类型安全的 Store(TypeScript)
1// stores/types.ts 2export interface User { 3 id: number 4 name: string 5 email: string 6} 7 8export interface AuthState { 9 user: User | null 10 token: string 11} 12 13// stores/auth.ts 14import { defineStore } from 'pinia' 15import type { User, AuthState } from './types' 16 17export const useAuthStore = defineStore('auth', { 18 state: (): AuthState => ({ 19 user: null, 20 token: '' 21 }), 22 23 getters: { 24 isAuthenticated: (state): boolean => state.user !== null, 25 userName: (state): string => state.user?.name || '' 26 }, 27 28 actions: { 29 setAuth(user: User, token: string): void { 30 this.user = user 31 this.token = token 32 }, 33 34 clearAuth(): void { 35 this.user = null 36 this.token = '' 37 } 38 } 39}) 40
4. 测试 Store
1// stores/__tests__/counter.spec.js 2import { setActivePinia, createPinia } from 'pinia' 3import { useCounterStore } from '../counter' 4 5describe('Counter Store', () => { 6 beforeEach(() => { 7 setActivePinia(createPinia()) 8 }) 9 10 test('increment', () => { 11 const store = useCounterStore() 12 expect(store.count).toBe(0) 13 14 store.increment() 15 expect(store.count).toBe(1) 16 }) 17 18 test('doubleCount getter', () => { 19 const store = useCounterStore() 20 store.count = 4 21 expect(store.doubleCount).toBe(8) 22 }) 23}) 24
总结
什么时候使用哪种状态管理?
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单组件状态 | 组件内 ref/reactive | 简单直接 |
| 少量组件共享 | 响应式全局对象 | 快速实现 |
| 中型应用 | Pinia (组合式) | 类型安全,易于测试 |
| 大型企业应用 | Pinia + 严格模式 | 可维护性,团队协作 |
核心原则
- 单一数据源:全局状态集中管理
- 状态只读:通过 actions 修改状态
- 纯函数修改:相同的输入总是得到相同的输出
- 不可变更新:不直接修改原状态,而是创建新状态
《Vue3 状态管理完全指南:从响应式 API 到 Pinia》 是转载文章,点击查看原文。
