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