跳转到内容

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

Vue 3 状态管理与 Pinia

状态管理是 Vue 应用开发的核心课题。从 Vue 2 的 Vuex 到 Vue 3 的 Pinia,再到 Composition API 的响应式 API,Vue 的状态管理方案不断演进。本文将深入响应式原理,系统讲解 Pinia 进阶用法、模块化设计、持久化存储及性能优化。


一、Vue 3 响应式原理

1.1 Proxy vs Object.defineProperty

typescript
// 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 的区别

typescript
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

typescript
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 的选择

typescript
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 安装与配置

bash
npm install pinia
typescript
// 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

typescript
// 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 在组件中使用

vue
<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 设计

typescript
// 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 间通信

typescript
// 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 状态持久化

typescript
// 使用 pinia-plugin-persistedstate
npm install pinia-plugin-persistedstate
typescript
// main.ts
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persistedstate)
typescript
// stores/user.ts - 配置持久化
export const useUserStore = defineStore('user', () => {
  // ...
}, {
  persist: {
    key: 'user-store',
    storage: localStorage,  // 默认 localStorage
    paths: ['name', 'email', 'isLoggedIn']  // 只持久化指定属性
  }
})

自定义持久化逻辑:

typescript
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 全局插件

typescript
// 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 状态重置

typescript
// 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 管理局部状态

typescript
// 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)

typescript
// 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 避免不必要的计算

typescript
// ❌ 每次渲染都会重新计算
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

typescript
// ❌ 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 优化列表渲染

vue
<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 实例

typescript
// 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 核心差异

特性VuexPinia
状态修改必须通过 mutations直接修改
模块化modules + namespaced天然模块化
TypeScript需要手动声明类型完整推断
API复杂(state/mutations/actions/getters)简洁(state/computed/actions)
代码组织单一 store 大文件每个 store 独立文件
插件系统有限强大

6.2 迁移指南

Vuex 代码:

typescript
// 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 等价代码:

typescript
// 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 迁移步骤

  1. 安装 Pinianpm install pinia
  2. 创建 Pinia 实例:在 main.ts 中配置
  3. 逐个迁移模块:将 Vuex modules 转换为 Pinia stores
  4. 更新组件引用this.$store.dispatch('user/login')useUserStore().login()
  5. 移除 Vuex:删除 Vuex 相关依赖和配置
  6. 测试验证:确保所有功能正常

七、最佳实践

7.1 Store 组织原则

  1. 单一职责:每个 store 只管理一个业务领域
  2. 扁平结构:避免嵌套 store
  3. 可复用性:将通用逻辑提取为 composables
  4. 测试友好:确保 store 可以独立测试

7.2 状态管理选择指南

场景推荐方案
全局共享状态Pinia
组件间通信props + emit 或 Pinia
页面级状态Composition API(composables)
表单状态组件局部状态
路由状态Vue Router query/params

7.3 常见错误

typescript
// ❌ 错误:直接解构会丢失响应式
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 的组合已经足够强大。


相关阅读: