Vue 3 状态管理模式与 Pinia 进阶实战 2026 | 响应式状态完全指南

状态管理是 Vue 应用开发的核心课题。从 Vue 2 的 Vuex 到 Vue 3 的 Pinia,再到 Composition API 的响应式 API,Vue 的状态管理方案不断演进。本文将深入响应式原理,系统讲解 Pinia 进阶用法、模块化设计、持久化存储及性能优化。
一、Vue 3 响应式原理
1.1 Proxy vs Object.defineProperty
// Vue 2: Object.defineProperty(只能监听属性访问)
const data = {}
Object.defineProperty(data, 'count', {
get() { /* 依赖收集 */ },
set() { /* 触发更新 */ }
})
// Vue 3: Proxy(可以监听属性增删、数组变化)
const proxy = new Proxy(data, {
get(target, key) { /* 依赖收集 */ },
set(target, key, value) { /* 触发更新 */ },
deleteProperty(target, key) { /* 触发更新 */ }
})1.2 ref 和 reactive 的区别
import { ref, reactive, watch, watchEffect } from 'vue'
// ref:用于基本类型和对象
const count = ref(0)
count.value++ // 需要 .value
const user = ref({ name: 'Alice' })
user.value.name = 'Bob' // 对象内部响应式
// reactive:仅用于对象
const state = reactive({ count: 0 })
state.count++ // 不需要 .value
// watch:监听特定数据源
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// watchEffect:自动追踪依赖
watchEffect(() => {
console.log(`当前 count: ${count.value}`)
})1.3 shallowRef 和 shallowReactive
import { shallowRef, shallowReactive } from 'vue'
// shallowRef:只监听 .value 本身的变化
const shallowUser = shallowRef({ name: 'Alice' })
shallowUser.value.name = 'Bob' // 不会触发更新!
shallowUser.value = { name: 'Bob' } // 会触发更新
// shallowReactive:只监听第一层属性变化
const shallowState = shallowReactive({
nested: { count: 0 }
})
shallowState.nested.count++ // 不会触发更新!
shallowState.nested = { count: 1 } // 会触发更新1.4 computed 和 watch 的选择
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed:用于派生值,有缓存
const fullName = computed(() => {
console.log('计算 fullName')
return `${firstName.value} ${lastName.value}`
})
// watch:用于副作用,无缓存
watch([firstName, lastName], ([newFirst, newLast]) => {
console.log(`名字变为: ${newFirst} ${newLast}`)
// 发送到服务器、更新 localStorage 等
})二、Pinia 基础
2.1 什么是 Pinia
Pinia 是 Vue 官方推荐的状态管理库,具有以下特点:
- 直观的 API(类似组件)
- 完整的 TypeScript 支持
- 模块化设计(每个 store 独立)
- 无 mutations(直接修改状态)
- 支持插件系统
2.2 安装与配置
npm install pinia// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')2.3 创建 Store
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态(state)
const name = ref('')
const email = ref('')
const isLoggedIn = ref(false)
// 计算属性(getters)
const displayName = computed(() => {
return name.value || 'Guest'
})
// 方法(actions)
async function login(username: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
})
const data = await response.json()
name.value = data.name
email.value = data.email
isLoggedIn.value = true
}
function logout() {
name.value = ''
email.value = ''
isLoggedIn.value = false
}
return { name, email, isLoggedIn, displayName, login, logout }
})2.4 在组件中使用
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 直接访问状态
console.log(userStore.name)
// 调用方法
await userStore.login('admin', 'password')
// 解构状态(保持响应式)
import { storeToRefs } from 'pinia'
const { name, isLoggedIn } = storeToRefs(userStore)
</script>三、Pinia 进阶技巧
3.1 模块化 Store 设计
// stores/index.ts - 导出所有 store
export { useUserStore } from './user'
export { useCartStore } from './cart'
export { useProductStore } from './product'
// stores/user.ts - 用户模块
export const useUserStore = defineStore('user', () => { /* ... */ })
// stores/cart.ts - 购物车模块
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const totalCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
function addItem(product: Product) {
const existing = items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
items.value.push({ ...product, quantity: 1 })
}
}
function removeItem(productId: string) {
const index = items.value.findIndex(item => item.id === productId)
if (index !== -1) {
items.value.splice(index, 1)
}
}
function clearCart() {
items.value = []
}
return { items, totalPrice, totalCount, addItem, removeItem, clearCart }
})3.2 Store 间通信
// stores/order.ts - 订单模块依赖购物车模块
import { defineStore } from 'pinia'
import { useCartStore } from './cart'
export const useOrderStore = defineStore('order', () => {
const orders = ref<Order[]>([])
async function createOrder() {
const cartStore = useCartStore()
if (cartStore.totalCount === 0) {
throw new Error('购物车为空')
}
const order: Order = {
id: Date.now().toString(),
items: [...cartStore.items],
total: cartStore.totalPrice,
createdAt: new Date()
}
await fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(order)
})
orders.value.push(order)
cartStore.clearCart() // 清空购物车
}
return { orders, createOrder }
})3.3 状态持久化
// 使用 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate// main.ts
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(persistedstate)// stores/user.ts - 配置持久化
export const useUserStore = defineStore('user', () => {
// ...
}, {
persist: {
key: 'user-store',
storage: localStorage, // 默认 localStorage
paths: ['name', 'email', 'isLoggedIn'] // 只持久化指定属性
}
})自定义持久化逻辑:
export const useUserStore = defineStore('user', () => {
const name = ref('')
// 从 localStorage 初始化
if (localStorage.getItem('user-name')) {
name.value = localStorage.getItem('user-name')!
}
// watch 监听变化自动保存
watch(name, (newVal) => {
localStorage.setItem('user-name', newVal)
})
return { name }
})3.4 全局插件
// plugins/logger.ts
export function loggerPlugin() {
return (context: PiniaPluginContext) => {
const { store } = context
// 记录所有 action 调用
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia] ${store.$id}.${name} 被调用,参数:`, args)
after((result) => {
console.log(`[Pinia] ${store.$id}.${name} 成功,返回:`, result)
})
onError((error) => {
console.error(`[Pinia] ${store.$id}.${name} 失败:`, error)
})
})
}
}
// main.ts
pinia.use(loggerPlugin())3.5 状态重置
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('')
const email = ref('')
const isLoggedIn = ref(false)
// 定义初始状态
const resetState = () => {
name.value = ''
email.value = ''
isLoggedIn.value = false
}
return { name, email, isLoggedIn, resetState }
})
// 使用
const userStore = useUserStore()
userStore.resetState()
// 或使用内置方法(需要定义 $reset)
userStore.$reset()四、组合式状态管理模式
4.1 使用 Composition API 管理局部状态
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return { count, doubled, increment, decrement, reset }
}
// 在组件中使用
<script setup>
const { count, doubled, increment } = useCounter(10)
</script>4.2 组合式 Store(无 Pinia)
// stores/todo.ts - 使用 reactive 实现轻量级状态管理
import { reactive, computed } from 'vue'
const state = reactive({
todos: [] as Todo[],
filter: 'all' as 'all' | 'active' | 'completed'
})
export function useTodoStore() {
const filteredTodos = computed(() => {
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
}
})
const activeCount = computed(() =>
state.todos.filter(todo => !todo.completed).length
)
function addTodo(text: string) {
state.todos.push({
id: Date.now(),
text,
completed: false
})
}
function toggleTodo(id: number) {
const todo = state.todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
function setFilter(filter: 'all' | 'active' | 'completed') {
state.filter = filter
}
return {
todos: () => state.todos,
filter: () => state.filter,
filteredTodos,
activeCount,
addTodo,
toggleTodo,
setFilter
}
}五、性能优化
5.1 避免不必要的计算
// ❌ 每次渲染都会重新计算
const expensiveResult = computed(() => {
// 耗时操作
return heavyComputation(state.data)
})
// ✅ 使用缓存
import { ref, computed } from 'vue'
const cache = ref<Map<string, Result>>(new Map())
const expensiveResult = computed(() => {
const key = JSON.stringify(state.data)
if (cache.value.has(key)) {
return cache.value.get(key)!
}
const result = heavyComputation(state.data)
cache.value.set(key, result)
return result
})5.2 使用 watch 替代 computed
// ❌ computed 用于副作用
const saveToServer = computed(() => {
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(state.data)
})
})
// ✅ 使用 watch
watch(() => state.data, async (newData) => {
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(newData)
})
}, { deep: true })5.3 优化列表渲染
<script setup>
import { computed } from 'vue'
import { useProductStore } from '@/stores/product'
const productStore = useProductStore()
// 只计算一次
const products = computed(() => [...productStore.products])
</script>
<template>
<!-- 使用 v-for 时提供唯一 key -->
<div v-for="product in products" :key="product.id">
{{ product.name }}
</div>
</template>5.4 缓存 Store 实例
// stores/user.ts
let userStoreInstance: ReturnType<typeof useUserStore> | null = null
export function useUserStore() {
if (!userStoreInstance) {
userStoreInstance = defineStore('user', () => { /* ... */ })()
}
return userStoreInstance
}六、Pinia vs Vuex
6.1 核心差异
| 特性 | Vuex | Pinia |
|---|---|---|
| 状态修改 | 必须通过 mutations | 直接修改 |
| 模块化 | modules + namespaced | 天然模块化 |
| TypeScript | 需要手动声明类型 | 完整推断 |
| API | 复杂(state/mutations/actions/getters) | 简洁(state/computed/actions) |
| 代码组织 | 单一 store 大文件 | 每个 store 独立文件 |
| 插件系统 | 有限 | 强大 |
6.2 迁移指南
Vuex 代码:
// store/modules/user.ts
const state = {
name: '',
isLoggedIn: false
}
const mutations = {
SET_NAME(state, name) {
state.name = name
},
LOGIN(state) {
state.isLoggedIn = true
},
LOGOUT(state) {
state.name = ''
state.isLoggedIn = false
}
}
const actions = {
async login({ commit }, { username, password }) {
const data = await api.login(username, password)
commit('SET_NAME', data.name)
commit('LOGIN')
}
}
const getters = {
displayName: state => state.name || 'Guest'
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}Pinia 等价代码:
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const name = ref('')
const isLoggedIn = ref(false)
const displayName = computed(() => name.value || 'Guest')
async function login(username: string, password: string) {
const data = await api.login(username, password)
name.value = data.name
isLoggedIn.value = true
}
function logout() {
name.value = ''
isLoggedIn.value = false
}
return { name, isLoggedIn, displayName, login, logout }
})6.3 迁移步骤
- 安装 Pinia:
npm install pinia - 创建 Pinia 实例:在
main.ts中配置 - 逐个迁移模块:将 Vuex modules 转换为 Pinia stores
- 更新组件引用:
this.$store.dispatch('user/login')→useUserStore().login() - 移除 Vuex:删除 Vuex 相关依赖和配置
- 测试验证:确保所有功能正常
七、最佳实践
7.1 Store 组织原则
- 单一职责:每个 store 只管理一个业务领域
- 扁平结构:避免嵌套 store
- 可复用性:将通用逻辑提取为 composables
- 测试友好:确保 store 可以独立测试
7.2 状态管理选择指南
| 场景 | 推荐方案 |
|---|---|
| 全局共享状态 | Pinia |
| 组件间通信 | props + emit 或 Pinia |
| 页面级状态 | Composition API(composables) |
| 表单状态 | 组件局部状态 |
| 路由状态 | Vue Router query/params |
7.3 常见错误
// ❌ 错误:直接解构会丢失响应式
const { name } = useUserStore() // name 不是响应式的
// ✅ 正确:使用 storeToRefs
const { name } = storeToRefs(useUserStore())
// ❌ 错误:在 action 中使用 await 但没有 async
function fetchData() {
await api.getData() // 报错!
}
// ✅ 正确:action 必须是 async
async function fetchData() {
await api.getData()
}
// ❌ 错误:在组件外调用 store 方法时没有初始化
// 必须在组件或 setup 函数中调用 useStore()八、总结
- ✅ 深入理解 Vue 3 响应式原理(Proxy、ref、reactive)
- ✅ 掌握 Pinia 基础用法(state、computed、actions)
- ✅ 实战模块化 Store 设计与 Store 间通信
- ✅ 实现状态持久化与全局插件
- ✅ 使用 Composition API 管理局部状态
- ✅ 性能优化技巧(缓存、watch、列表渲染)
- ✅ Pinia 与 Vuex 对比及迁移指南
状态管理的核心原则是:保持简单,按需选择。对于大多数应用,Pinia + Composition API 的组合已经足够强大。
相关阅读: