Vue3 状态管理完全指南:从响应式 API 到 Pinia

什么是状态管理?

在 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 + 严格模式 可维护性,团队协作

核心原则

  1. 单一数据源:全局状态集中管理
  2. 状态只读:通过 actions 修改状态
  3. 纯函数修改:相同的输入总是得到相同的输出
  4. 不可变更新:不直接修改原状态,而是创建新状态
相关推荐
90后的晨仔4 小时前
Vue 内置组件全解析:提升开发效率的五大神器
前端·vue.js
我胡为喜呀4 小时前
Vue3 中的 watch 和 watchEffect:如何优雅地监听数据变化
前端·javascript·vue.js
我登哥MVP4 小时前
Ajax 详解
java·前端·ajax·javaweb
非凡ghost5 小时前
Typora(跨平台MarkDown编辑器) v1.12.2 中文绿色版
前端·windows·智能手机·编辑器·软件需求
馨谙5 小时前
/dev/null 是什么,有什么用途?
前端·chrome
JamSlade6 小时前
流式响应 sse 系统全流程 react + fastapi为例子
前端·react.js·fastapi
徐同保6 小时前
react useState ts定义类型
前端·react.js·前端框架
liangshanbo12156 小时前
React 19 vs React 18全面对比
前端·javascript·react.js
望获linux6 小时前
【实时Linux实战系列】Linux 内核的实时组调度(Real-Time Group Scheduling)
java·linux·服务器·前端·数据库·人工智能·深度学习