Vue2 的 Vuex 状态管理与 Vue3 的 Pinia 状态管理

1. 引言:为什么需要状态管理

在 Vue 开发中,多个组件经常需要共享同一份数据,比如用户登录信息、购物车数据、全局主题配置等。如果每个组件都各自维护一份,数据就会不一致,改起来也很麻烦。状态管理工具就是用来统一存放和管理这些共享数据的。

在 Vue2 时代,大家最熟悉的状态管理工具是 Vuex;到了 Vue3,官方更推荐使用 Pinia。本文就用通俗易懂的方式,结合 Vue2 的 Vuex 做对比,讲清楚 Pinia 的作用,并带大家写一个入门案例。

2. Pinia 是什么

Pinia 是 Vue 官方推出的新一代状态管理库,可以把它理解为 Vuex 的「升级版」和「简化版」。它保留了 Vuex 的核心思想------把共享数据集中管理,同时大幅简化了使用方式,让代码更简洁、类型提示更友好。

简单来说,Pinia 就是帮你把「多个组件都要用的数据」集中放在一个地方,谁要用就去取,谁要改就去改,改完所有用到的地方自动更新。

3. Pinia 与 Vuex 的核心对比

为了更直观地理解 Pinia 的优势,下面从几个关键维度对比 Vue2 的 Vuex 和 Vue3 的 Pinia。

对比维度 Vuex(Vue2 常用) Pinia(Vue3 推荐)
核心概念 state、mutations、actions、getters、modules state、getters、actions(去掉了 mutations)
修改数据 必须通过 mutations 同步修改,流程较繁琐 可以直接修改 state,也支持 actions 处理异步
TypeScript 支持 支持但配置较复杂 天然友好,类型推断完善
模块化 需要嵌套 modules,结构较重 每个 store 独立定义,天然模块化
代码量 样板代码多,写起来啰嗦 简洁直观,上手快

从表格可以看出,Pinia 最大的变化就是去掉了 mutations。在 Vuex 中,改数据要先写 mutations,再通过 commit 触发;而 Pinia 允许直接修改 state,也可以像 Vuex 的 actions 一样处理异步逻辑,心智负担小了很多。

4. Vuex 的核心概念

在对比完 Pinia 和 Vuex 之后,我们先来把 Vuex 的核心概念讲清楚,这样后续再看 Pinia 的入门案例时,就能更深刻地体会到两者的差异。Vuex 主要围绕五个核心概念展开。

4.1 state(状态)

state 是 Vuex 中存放共享数据的地方,相当于组件里的 data。它是单一数据源,所有组件需要共享的数据都集中放在这里。组件通过 this.$store.state 来读取。

4.2 getters(计算属性)

getters 相当于组件里的 computed,用于根据 state 派生出新数据。比如根据商品价格和数量计算总价,就可以写在 getters 里,多处复用,避免重复计算。

4.3 mutations(变更)

mutations 是 Vuex 中唯一允许同步修改 state 的地方。修改数据必须通过 commit 触发一个 mutation,这是 Vuex 比较严格的地方,也是它流程繁琐的主要原因。mutation 里的函数必须是同步的。

4.4 actions(动作)

actions 用来处理异步逻辑,比如请求接口。它不能直接修改 state,而是通过 commit 调用 mutation 来间接修改。actions 里可以写 await 等异步代码。

4.5 modules(模块)

当项目变大、状态很多时,可以把 store 拆分成多个 modules,每个模块拥有自己的 state、getters、mutations 和 actions。模块化让结构更清晰,但也增加了嵌套层级,使用起来相对较重。

简单总结一下 Vuex 的数据流:组件通过 dispatch 触发 actions,actions 处理异步后 commit 到 mutations,mutations 同步修改 state,state 变化后驱动视图更新。这个单向数据流保证了状态变更可追踪,但也带来了较多的样板代码。

5. Vuex 入门案例:实现计数器

为了更直观地对比,下面用 Vue2 的 Vuex 实现同样的计数器,大家可以对照着看两套写法的差异。先看一下 Vuex 项目的目录结构:

bash 复制代码
src/
├── main.js
├── App.vue
├── store/
│   └── index.js
└── components/
    └── Counter.vue

5.1 安装 Vuex

在 Vue2 项目中安装 Vuex 3.x 版本。

bash 复制代码
npm install vuex@3

5.2 创建 store

新建 store/index.js 文件,定义 state、getters、mutations 和 actions。

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount: (state) => state.count * 2
  },
  mutations: {
    increment(state) {
      state.count++
    },
    setCount(state, value) {
      state.count = value
    }
  },
  actions: {
    increment({ commit }) {
      commit('increment')
    },
    async fetchCount({ commit }) {
      // 模拟异步请求
      const res = await new Promise((resolve) => {
        setTimeout(() => resolve(10), 1000)
      })
      commit('setCount', res)
    }
  }
})

5.3 在入口文件中注册

在 main.js 中把 store 挂载到 Vue 实例上。

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({
  store,
  render: (h) => h(App)
}).$mount('#app')

5.4 在组件中使用

在组件中通过 this.$store 读取状态,并通过 dispatch 触发 actions。

vue 复制代码
<template>
  <div>
    <p>当前计数:{{ $store.state.count }}</p>
    <p>双倍计数:{{ $store.getters.doubleCount }}</p>
    <button @click="$store.dispatch('increment')">加一</button>
    <button @click="$store.dispatch('fetchCount')">异步更新</button>
  </div>
</template>

<script>
export default {
  name: 'Counter'
}
</script>

可以发现:Vuex 修改数据必须经过 mutations 和 commit,异步逻辑还要再包一层 actions,样板代码明显更多;

6. Pinia 的核心概念

Pinia 主要围绕三个核心概念展开,理解它们就掌握了 Pinia 的八成用法。

6.1 state(状态)

state 就是存放共享数据的地方,相当于组件里的 data。定义时用箭头函数返回一个对象即可。

6.2 getters(计算属性)

getters 相当于组件里的 computed,用于根据 state 派生出新数据。比如根据商品价格和数量计算总价,就可以写在 getters 里,多处复用。

6.3 actions(动作)

actions 用来封装修改 state 的逻辑,尤其是异步操作,比如请求接口后把结果写入 state。在 Pinia 中,actions 里可以直接修改 state,不需要像 Vuex 那样再走 mutations。

6.4 修改数据的三种方式

在 Pinia 中,修改 state 数据非常灵活,主要有三种方式。下面结合计数器案例逐一说明,并与 Vuex 的写法做对比。

方式一:直接修改 state

Pinia 允许在组件里直接给 state 属性赋值,这是最简单的方式。Vuex 必须通过 commit 触发 mutation 才能修改,而 Pinia 省去了这一层。

javascript 复制代码
// Pinia:直接修改
counter.count++

// Vuex:必须走 mutation
this.$store.commit('increment')   // this是当前store对象
方式二:通过 actions 修改

当修改逻辑较复杂或涉及异步操作时,推荐在 actions 里封装。Pinia 的 actions 里可以直接修改 state,而 Vuex 的 actions 只能通过 commit 调用 mutation 间接修改。

javascript 复制代码
// Pinia:actions 里直接改 state
actions: {
  increment() {
    this.count++   // this是当前store对象
  },
  async fetchCount() {
    const res = await api.getCount()
    this.count = res
  }
}

// Vuex:actions 里 commit 到 mutation
actions: {
  increment({ commit }) {
    commit('increment')
  },
  async fetchCount({ commit }) {
    const res = await api.getCount()
    commit('setCount', res)
  }
}
方式三:通过 $patch 批量修改

当需要同时修改多个 state 属性时,可以用 $patch 传入一个对象或函数,一次性完成批量更新。这是 Pinia 独有的能力,Vuex 没有对应的简洁写法。

javascript 复制代码
// 对象写法:一次修改多个属性
counter.$patch({
  count: 10,
  name: '张三'
})

// 函数写法:适合带逻辑的批量修改
counter.$patch((state) => {
  state.count += 1
  state.name = '李四'
})

对比小结:Vuex 修改数据必须经过 mutations 和 commit,流程固定但样板代码多;Pinia 则提供了直接赋值、actions、$patch 三种灵活方式,既保留了可追踪性,又大幅简化了写法。

7. Pinia入门案例:实现计数器

下面通过一个最简单的计数器案例,带大家完整走一遍 Pinia 的使用流程。先看一下项目的目录结构:

bash 复制代码
src/
├── main.js
├── App.vue
├── store/
│   └── counter.js
└── components/
    └── Counter.vue

7.1 安装 Pinia

首先在项目中安装 Pinia。

bash 复制代码
npm install pinia

7.2 在入口文件中注册

在 main.js 中创建 Pinia 实例并挂载到 Vue 应用上。

javascript 复制代码
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')

7.3 定义一个 store

新建 store/counter.js 文件,用 defineStore 定义一个计数器 store。

注意:

store暴露变量名官方命名规范是 userXXXStore。defineStore有两个参数(分类id, 对象。在pinia调试工具会显示分类id。

vue3 pinia 的 state和 vue2 vuex的state有区别,vue2是对象形式写法,而vue3要写成函数。

javascript 复制代码
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchCount() {
      // 模拟异步请求
      const res = await new Promise((resolve) => {
        setTimeout(() => resolve(10), 1000)
      })
      this.count = res
    }
  }
})

7.4 在组件中使用

在组件中引入 store,即可读取和修改状态。

javascript 复制代码
<template>
  <div>
    <p>当前计数:{{ counter.count }}</p>
    <p>双倍计数:{{ counter.doubleCount }}</p>
    <button @click="counter.increment()">加一</button>
    <button @click="counter.fetchCount()">异步更新</button>
  </div>
</template>

<script setup>
import { useCounterStore } from './store/counter'

const counter = useCounterStore()

// 下面两种方式都可以拿到state数据
// console.log(1, counter.count)
// console.log(2, counter.$state.count)
</script>

运行后点击「加一」按钮,计数会同步增加;点击「异步更新」按钮,一秒后计数会变成 10。多个组件只要都调用 useCounterStore,就能共享同一个 count,一处修改处处更新。

扩展

我们都知道通过ref包裹的响应式数据都需要 属性名.value才能获取到值。但是通过reactive中包含ref的值不需要再.vlaue获取值,会自动帮我们拆包。

javascript 复制代码
let obj = reactive({
    a: 1,
    b: 2,
    c: ref(3)
})

console.log(obj.a)  // 输出结果1
console.log(obj.b)  // 输出结果2
console.log(obj.c)  // 输出结果3


let x = ref(4)

console.log(x)  // 输出结果 ref(4) 的实例对象
console.log(x.value)  // 输出结果4

8. Pinia 进阶:storeToRefs、getters、$subscribe 与组合式写法

掌握了 Pinia 的基础用法后,本节再深入几个高频知识点:storeToRefs 与 toRefs 的区别、getters 的进阶使用、$subscribe 订阅监听,以及 store 的组合式写法。下面逐一讲解并给出带注释的案例。

8.1 storeToRefs 与 toRefs 的区别

在组件中使用 store 时,很多人会直接解构 store,但这样会丢失响应式。Vue 提供了 toRefs,Pinia 则提供了更合适的 storeToRefs,两者有本质区别。

toRefs 是 Vue 自带的工具函数,它会把一个响应式对象的所有属性都转成 ref。但直接对 store 使用 toRefs 时,会把 store 上的 actions 等非 state 属性也一并转成 ref,导致方法丢失响应式上下文,调用会报错。

storeToRefs 是 Pinia 专门提供的工具函数,它只会把 store 里的 state 和 getters 转成 ref,actions 方法保持原样,不会被打包成 ref。因此解构 store 时,应该用 storeToRefs 来取 state 和 getters,用普通解构来取 actions。

javascript 复制代码
// 错误示范:直接解构 store,会丢失响应式
const { count, doubleCount } = useCounterStore()
// count 变成普通值,修改后视图不会更新

// 错误示范:对 store 使用 toRefs,会把 actions 也转成 ref
const { count, increment } = toRefs(useCounterStore())
// increment 变成 ref 包裹的函数,调用时 this 指向丢失,会报错

// 正确示范:用 storeToRefs 取 state 和 getters,普通解构取 actions
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
// storeToRefs 只转换 state 和 getters,保持响应式
const { count, doubleCount } = storeToRefs(counter)
// actions 用普通解构,保持方法上下文
const { increment, fetchCount } = counter

8.2 getters 的进阶使用

getters 相当于组件里的 computed,用于根据 state 派生出新数据。除了基础用法,getters 还支持通过 this 访问其他 getters,以及返回函数实现带参数的 getter。

javascript 复制代码
// store/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    base: 10
  }),
  getters: {
    // 基础用法:接收 state 作为参数
    doubleCount: (state) => state.count * 2,

    // 进阶用法:通过 this 访问其他 getters
    // 注意:使用 this 时不能写成箭头函数,要用普通函数
    doubleCountPlusBase() {
      // this 指向当前 store,可以拿到 state 和其他 getters
      return this.doubleCount + this.base
    },

    // 进阶用法:返回一个函数,实现带参数的 getter
    // 适合根据传入参数动态计算,比如按倍数计算
    multiplyBy: (state) => {
      return (n) => state.count * n
    }
  }
})
javascript 复制代码
// 组件中使用
import { useCounterStore } from './store/counter'
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
// 用 storeToRefs 保持 getters 的响应式
const { doubleCount, doubleCountPlusBase } = storeToRefs(counter)

// 带参数的 getter 是函数,直接调用即可
console.log(counter.multiplyBy(3))  // 输出 count * 3 的结果

8.3 $subscribe 订阅监听

$subscribe 用于监听 store 中 state 的变化,类似 Vuex 的 subscribe。当 state 被修改时,会触发回调函数,适合做数据持久化、日志记录等场景。下面先给出 Pinia 的完整示例,再给出 Vue2 中 Vuex 的 subscribe 写法做对比。

javascript 复制代码
// 组件中订阅 state 变化(Pinia / Vue3)
import { useCounterStore } from './store/counter'

const counter = useCounterStore()

// $subscribe 接收一个回调,state 变化时触发
// 第一个参数是 mutation 信息,第二个参数是变化后的 state
counter.$subscribe((mutation, state) => {
  // mutation.type 表示修改方式:'direct' 直接修改、'patch object' 对象式 $patch、'patch function' 函数式 $patch
  console.log('修改方式:', mutation.type)
  // mutation.storeId 表示是哪个 store 发生了变化
  console.log('store id:', mutation.storeId)
  // state 是变化后的最新状态
  console.log('最新 count:', state.count)

  // 常见用途:把 state 持久化到 localStorage
  localStorage.setItem('counter', JSON.stringify(state))
})

// 触发一次修改,观察订阅回调
counter.increment()

下面是对应的 Vue2 + Vuex 写法。Vuex 使用 store.subscribe 订阅 mutation 提交,回调的第一个参数是 mutation 对象(包含 type 和 payload),第二个参数是变化后的根 state。需要注意的是,Vuex 的 subscribe 只能监听到通过 mutation 提交的修改,直接给 state 赋值不会触发回调。

javascript 复制代码
// 组件中订阅 state 变化(Vuex / Vue2)
// 通常在 created 生命周期里注册订阅
created() {
  // store.subscribe 接收一个回调,每次 mutation 提交后触发
  // 第一个参数是 mutation 对象,包含 type 和 payload
  // 第二个参数是变化后的根 state
  this.$store.subscribe((mutation, state) => {
    // mutation.type 表示触发的 mutation 名称,如 'increment'
    console.log('mutation 名称:', mutation.type)
    // mutation.payload 表示 commit 时传入的参数
    console.log('mutation 参数:', mutation.payload)
    // state 是变化后的根 state,需要按模块路径取值
    console.log('最新 count:', state.count)

    // 常见用途:把 state 持久化到 localStorage
    localStorage.setItem('counter', JSON.stringify(state))
  })
}

// 触发一次修改,观察订阅回调
this.$store.dispatch('increment')

对比小结:Pinia 的 subscribe 和 Vuex 的 subscribe 都能监听状态变化,但有两个明显差异。一是触发时机不同:Pinia 的 subscribe 在 state 被直接修改、$patch 或 action 修改时都会触发;而 Vuex 的 subscribe 只在 mutation 提交后触发,直接改 state 不会触发。二是回调参数不同:Pinia 的 mutation.type 会区分 direct、patch object、patch function 等修改方式,Vuex 的 mutation.type 则是 mutation 名称,payload 单独作为 mutation.payload 传入。

8.4 store 的组合式写法

除了选项式写法,Pinia 还支持组合式写法,类似 Vue3 的 setup 语法。用 ref 定义 state,用 computed 定义 getters,用普通函数定义 actions,结构更灵活,也更适合复杂逻辑的复用。下面先给出组合式写法的完整示例,再给出对应的选项式写法,方便大家对比两种写法的区别。

javascript 复制代码
// store/counter.js ------ 组合式写法
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // state:用 ref 定义,相当于选项式里的 state
  const count = ref(0)
  const name = ref('张三')

  // getters:用 computed 定义,相当于选项式里的 getters
  const doubleCount = computed(() => count.value * 2)

  // actions:用普通函数定义,相当于选项式里的 actions
  // 函数里可以直接通过 .value 修改 ref 数据
  function increment() {
    count.value++
  }

  // 异步 action:模拟请求后更新数据
  async function fetchCount() {
    const res = await new Promise((resolve) => {
      setTimeout(() => resolve(10), 1000)
    })
    count.value = res
  }

  // 把需要暴露的 state、getters、actions 全部返回
  return {
    count,
    name,
    doubleCount,
    increment,
    fetchCount
  }
})

下面是对应的选项式写法,两者实现的功能完全一致,方便对照理解。

javascript 复制代码
// store/counter.js ------ 选项式写法
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // state:用函数返回对象,相当于组合式里的 ref
  state: () => ({
    count: 0,
    name: '张三'
  }),
  // getters:用 computed 的语法糖,相当于组合式里的 computed
  getters: {
    doubleCount: (state) => state.count * 2
  },
  // actions:直接定义方法,相当于组合式里的普通函数
  actions: {
    increment() {
      this.count++
    },
    async fetchCount() {
      const res = await new Promise((resolve) => {
        setTimeout(() => resolve(10), 1000)
      })
      this.count = res
    }
  }
})
javascript 复制代码
// 组件中使用组合式 store
import { useCounterStore } from './store/counter'
import { storeToRefs } from 'pinia'

const counter = useCounterStore()
// 组合式写法下,storeToRefs 依然适用
const { count, doubleCount } = storeToRefs(counter)
const { increment, fetchCount } = counter

两种写法的区别:组合式写法在于defineStore第二个参数是函数,选项式写方法defineStore第二个参数是对象

相关推荐
greenbbLV1 小时前
中小公司积分商城选型:SaaS与私有化方案对比解析
大数据·前端·小程序
看昭奚恤哭2 小时前
Flutter 布局核心思想
开发语言·javascript·flutter
朝阳5812 小时前
Cesium `Camera.flyTo`:“飞过去“
javascript
NutShell Wang3 小时前
Vite 8.1 深度拆解:Rolldown 统一打包器如何终结前端构建的「双引擎时代」
前端·rust·开源·vite·开发者工具·vibe coding
朱容zr3331333 小时前
请解释“回表”的概念。
java·前端·数据库
leslie1183 小时前
webpack笔记
前端·webpack
宁风NF3 小时前
JavaScript:内存、垃圾回收、性能优化
开发语言·前端·javascript·学习·性能优化·es6
勾勾圈圈蛋蛋3 小时前
DOM、BOM、DOM操作详解 + 结合Vue响应式关联梳理
前端
打妖妖灵滴哪吒4 小时前
IOS模态窗口的作用与设计原则
前端·ui