一文带你了解前端全局状态管理

125 阅读18分钟

一文带你彻底搞懂 Vue 全局状态管理:从 Vuex 到 Pinia

很多同学刚接触状态管理的时候,脑子里通常只有两个问题:

  1. 这玩意到底有没有必要?
  2. Vuex、Pinia 这些库到底是在解决什么问题?

表面看它们是在“集中存数据”,但真到项目里你会发现,状态管理真正要解决的,从来不只是“存”,而是下面这几件事:

  • 一份状态到底该由谁维护;
  • 这份状态允许谁改;
  • 改动路径是否统一;
  • 出问题时能不能追踪;
  • 复杂业务里,多组件之间能不能低成本复用同一套逻辑。

前言:前端真的需要全局状态管理吗?

我个人的看法是:需要,但不是所有状态都值得上全局。

这句话听起来像一句废话,但其实挺关键。很多同学一提全局状态管理,就容易走向两个极端:

  • 要么什么都往 store 里塞,连弹窗开关都想丢进去;
  • 要么完全排斥 store,觉得 props 和 emit 也能传,何必再加一层抽象。

这两个方向都不太对。

对于局部状态,比如一个表单项是否聚焦、某个下拉框是否展开,这种状态天然就属于组件自己,放组件内部最合适,简单直接,不需要上升到全局。

但如果你的项目开始出现下面这些情况,那全局状态管理往往就不是“要不要”的问题,而是“你准备什么时候补票”的问题:

  • 用户信息、权限信息、购物车、主题配置等数据会被多个页面重复使用;
  • 同一份业务数据会被多个层级的组件共享;
  • 一个状态的更新会牵动多个区域联动刷新;
  • 异步请求、缓存、埋点、副作用逻辑开始不断增多;
  • 出 bug 之后,大家都在问一句经典台词:这数据是谁改的?

说白了,状态管理不是为了炫技,而是为了让数据流动有规矩。

本文会从下面几个方向来聊这件事:

  • 先看 Vuex 为什么会出现,它主要解决了什么问题;
  • 再看 Vuex 的核心概念,以及它的使用方式;
  • 然后再聊 Pinia 为什么后来成为 Vue 官方推荐方案;
  • 最后结合 Vuex 和 Pinia 的差异,看看今天的项目到底该怎么选。

1. 背景:为什么全局状态管理会成为一个问题?

在前端开发的早期,页面状态往往比较简单。很多场景下,大家用几个全局变量、几个事件监听、再配一点 DOM 操作,居然也能把业务跑起来。

但随着 SPA 普及、页面交互复杂度提升、组件化开发成为主流后,问题就逐渐开始冒头了:

  • 数据分散在不同组件和接口回调里;
  • 状态既可能被父组件改,也可能被子组件改;
  • 同一份数据在多个地方维护副本;
  • 异步逻辑和 UI 逻辑搅成一锅粥。

最后你会得到一种熟悉的项目气味:

数据能跑,但没人说得清它是怎么跑起来的。

为了应对这些问题,业界逐渐形成了一套相对统一的思路:

  • 单向数据流:数据更新尽量沿一个固定方向流动;
  • 集中式或半集中式存储:把跨组件共享的数据收拢起来;
  • 可追踪的状态变更:保证“谁在什么时候改了什么”是可以被观察到的。

这也是为什么后来会出现 Redux、MobX、Vuex、Pinia、Zustand、Recoil 这类状态管理方案。它们 API 各不相同,但核心目标其实差不多:

让状态不再乱跑。


2. Vuex

2.1 Vuex 主要是为了解决什么问题?

如果大家经历过 Vue 2 时代的中大型项目,大概率都能感受到:当应用稍微变大一点,状态共享这件事会立刻变得很别扭。

最常见的几个痛点,大概是下面这些。

1)跨组件共享状态非常麻烦

usertokencart 这类数据,经常会在很多页面和组件里都要用到。

对于简单的父子组件,你还能靠 props 往下传;但一旦层级变深,问题就来了。你会发现自己开始做一件很机械、也很容易出错的事情:

为了给最底层组件传一个值,你要把中间整条链路都改一遍。

更离谱一点的项目,还会引入 event bus,到处 $emit / $on,最后组件之间像地下接头一样彼此通风报信。

image.png

2)状态修改没有约束,调试困难

很多项目在没有状态管理的时候,往往会出现这些写法:

  • window.user
  • this.$root.xxx
  • 一个被多个模块共享的全局对象

这些方案最大的问题不是“不能用”,而是谁都能改,改了还不容易追踪

而且 Vue 2 还有一个经典老坑:

  • window.user 这类对象不在 Vue 的响应式体系里,改了不一定触发视图更新;
  • 给响应式对象新增属性时,还得配合 Vue.set / this.$set 才能被追踪。

这种时候,项目最常出现的一句话就是:

页面怎么没更新?

然后排查半天,发现不是没改,而是你改的地方根本没被响应式系统认出来。

3)复杂业务的异步逻辑散落在各处

状态共享一旦复杂起来,请求逻辑、缓存逻辑、权限逻辑、兜底逻辑,就很容易跟组件本身的展示逻辑缠在一起。

结果就是:

  • 一份业务规则在多个组件里重复写;
  • 同一段请求逻辑被拷贝很多次;
  • 后续想复用、测试、排错都会越来越痛苦。

4)Vuex 的思路是什么?

Vuex 的思路很直接:

  • 统一搞一个全局 store
  • 所有共享状态都放进 state
  • 同步修改走 mutation
  • 异步逻辑和业务流程放进 action
  • 派生状态交给 getter
  • 再配合 DevTools,把每次状态修改记录下来。

它本质上是在给项目立规矩:

数据统一放,修改统一走,出了问题统一查。

这个规矩听起来有点“教条”,但在多人协作和复杂业务里,这种教条往往恰恰是秩序的来源。


2.2 Vuex 基本概念

1)Vuex 是什么?

Vuex 是一个专为 Vue 应用设计的状态管理模式 + 库。它采用集中式存储的方式来管理应用中所有组件的共享状态,并且通过一套明确规则,让状态的变化变得可预测。

它背后的设计思想,和 Flux 架构有非常深的关系。

2)Flux 是什么?

Flux 是 Facebook 在构建大型 Web 应用时提出的一种状态管理架构。它最核心的目标,就是解决复杂场景下的数据一致性问题。

很多前端状态管理工具,底层思路其实都和 Flux 有亲缘关系,比如 Redux、Vuex、Pinia,甚至很多轻量状态库都能看到它的影子。

image.png Flux 里有四个经典角色:

  • store:存储状态的地方;
  • view:视图层,根据状态渲染页面;
  • action:描述一次行为;
  • dispatcher:负责把 action 分发出去。

image.png

3)单向数据流到底是什么意思?

很多文章都提“单向数据流”,但一讲就容易虚。

我的理解很简单:

应用里数据的产生、修改、再反馈到界面的路径,尽量只有一个固定方向。

放到 Vuex 里,可以理解成这样一条链路:

View(用户操作) → Action(表达意图) → Mutation(修改状态) → State 更新 → View 重新渲染

image.png

你可以把它想成一条高速公路:

车都按一个方向走,别有人在路上逆行。

一旦逆行,也就是多个地方绕过规则直接改状态,数据为什么错、视图为什么乱、逻辑为什么互相打架,这些问题就会一起冒出来。

4)Vuex 的核心组成

Vuex 在沿用 Flux 思路的基础上,结合 Vue 的特点做了一层封装。它主要由下面几部分组成:

image.png

State

全局状态本体。Vuex 使用的是单一状态树,也就是整个应用原则上只维护一个 store 实例。

它的好处是:任意状态都能从同一个入口找到。

Getter

当我们需要从原始状态里计算出一些“派生状态”时,就会用到 getter。

它和组件里的计算属性很像:

  • 会基于已有 state 计算新结果;
  • 会自动收集依赖;
  • 在依赖不变时可以复用结果。

一个很重要的习惯是:

state 里尽量只放“最小必要原始状态”,能算出来的就不要重复存。

Mutation

Vuex 规定:修改 state 的唯一方式是提交 mutation。

而且 mutation 必须是同步的。

为什么要强调同步?因为 DevTools 需要精确记录一次状态变化对应的时机和结果。如果 mutation 里混入异步逻辑,状态追踪就会变得不可靠。

Action

异步逻辑、复杂业务流程、多个 mutation 的组合调度,通常都放在 action 里。

action 自己不直接改状态,而是通过 commit 去提交 mutation。

最经典的场景,就是请求接口之后再更新数据。

actions: {
  async checkout({ commit }, products) {
    commit('checkoutStart')
    try {
      await shop.buyProducts(products)
      commit('checkoutSuccess')
    } catch (e) {
      commit('checkoutFail')
    }
  }
}
Module

随着项目变大,所有状态都堆在一个 store 里,最终一定会越来越臃肿。

所以 Vuex 提供了 modules,允许我们把 store 拆成多个模块。每个模块都可以拥有自己的:

  • state
  • getters
  • mutations
  • actions
  • 甚至嵌套子模块

这个能力非常实用,但也正是从这里开始,Vuex 的使用心智慢慢变重了。后面讲 Pinia 时,我们会再回到这里。


2.3 Vuex 的使用

这一节我不打算把所有示例都写成“完整项目模板”,因为那样代码会非常长,读起来也很容易疲劳。我们直接看关键骨架,抓重点就行。

1)最小可运行版本

先看一个最基础的 Vuex store:

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    sum: 0
  },
  mutations: {
    add(state, value) {
      state.sum += value
    }
  },
  actions: {
    addWait({ commit }, value) {
      setTimeout(() => commit('add', value), 500)
    }
  },
  getters: {
    bigSum(state) {
      return state.sum * 10
    }
  }
})

把它挂到入口即可:

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

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

组件里怎么用?先看最直接的版本:

<template>
  <div>
    <h1>当前求和为:{{ $store.state.sum }}</h1>
    <h1>当前大十倍:{{ $store.getters.bigSum }}</h1>
    <button @click="$store.commit('add', 1)">+1</button>
    <button @click="$store.dispatch('addWait', 1)">等 1 秒加 1</button>
  </div>
</template>

这个例子虽然简单,但已经把 Vuex 的核心角色串起来了:

  • state 存原始状态;
  • getter 存派生状态;
  • mutation 负责同步改值;
  • action 负责异步流程。

2)为什么 getter 有意义?

很多同学一开始会把所有值都直接塞进 state,比如既存 sum,又存 bigSum

这其实不太划算。

如果一个值可以由别的值推导出来,那它更适合放在 getter 里,而不是单独保存一份副本。因为一旦你维护两份同源数据,就要面临同步问题。

getter 的意义就在这里:

只保存原始状态,派生结果按需计算。

3)模块化:项目一大,拆是迟早的事

大型项目里,Vuex 一般会拆成这种结构:

store/
├── index.js
└── modules/
    ├── count.js
    └── code.js

index.js 只负责组装模块:

import Vue from 'vue'
import Vuex from 'vuex'
import countModules from './modules/count'
import codeModules from './modules/code'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    countModules,
    codeModules
  }
})

模块本身的写法和根 store 很像:

// store/modules/count.js
export default {
  state: {
    sum: 0
  },
  getters: {
    bigSum(state) {
      return state.sum * 10
    }
  },
  mutations: {
    add(state, value) {
      state.sum += value
    }
  },
  actions: {
    addWait({ commit }, value) {
      setTimeout(() => commit('add', value), 500)
    }
  }
}

4)命名空间:不隔离,迟早撞车

模块一多,就会出现一个非常现实的问题:

大家都叫 add、都叫 bigSum,那你到底调用的是谁?

这时候 namespaced: true 就很重要了。

export default {
  namespaced: true,
  state: { sum: 0 },
  getters: {
    bigSum: state => state.sum * 10
  },
  mutations: {
    add(state, value) {
      state.sum += value
    }
  },
  actions: {
    addWait({ commit }, value) {
      setTimeout(() => commit('add', value), 500)
    }
  }
}

使用时就要带上模块路径:

<template>
  <div>
    <h1>{{ $store.state.countModules.sum }}</h1>
    <h1>{{ $store.getters['countModules/bigSum'] }}</h1>
    <button @click="$store.commit('countModules/add', 1)">+1</button>
    <button @click="$store.dispatch('countModules/addWait', 1)">等 1 秒加 1</button>
  </div>
</template>

这也是 Vuex 后来经常被吐槽的一点:

路径字符串一多,代码开始有点像在输快递单号。

5)map 辅助函数:少写点重复劳动

为了减少一堆 this.$store.xxx,Vuex 提供了四个辅助函数:

  • mapState
  • mapGetters
  • mapMutations
  • mapActions

比如下面这种写法,在 Options API 里就很常见:

import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState('countModules', ['sum']),
    ...mapGetters('countModules', ['bigSum'])
  },
  methods: {
    ...mapMutations('countModules', ['add']),
    ...mapActions('countModules', ['addWait'])
  }
}

这确实比手写一堆 $store 调用清爽不少。

不过它也有一个问题:这些辅助函数本质上是围绕 this.$store 设计的,所以到了 Vue 3 的 Composition API 里,就不再那么顺手了。

6)Vue 3 里怎么用 Vuex?

Vue 3 一般搭配 Vuex 4 使用。核心 API 不算大改,但写法会更偏向 Composition API。

<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()

const sum = computed(() => store.state.countModules.sum)
const bigSum = computed(() => store.getters['countModules/bigSum'])

const add = value => store.commit('countModules/add', value)
const addWait = value => store.dispatch('countModules/addWait', value)
</script>

这里有一个很容易踩的点:

const { sum } = store.state.countModules

这种直接解构出来的值,如果是基础类型,就不会继续保持响应式

所以在 Composition API 里,通常会更推荐:

  • computed
  • toRef
  • toRefs
  • watch

这些方式去处理响应式读取。

7)动态注册模块:能做,但心智不低

Vuex 还支持 registerModule 动态注册模块。

它适合一些按需加载场景,比如某个页面进入时才挂载模块,离开时再卸载。

if (!store.hasModule('codeModules')) {
  store.registerModule('codeModules', codeModules)
}

这个能力本身不差,问题在于:

  • 你要关心模块何时注册;
  • 你要关心模块是否重复注册;
  • 你还要考虑何时注销。

到了这里,Vuex 的“工程感”其实已经非常强了。它不是不能用,而是它开始要求开发者投入越来越多的心智来维护这套体系。

这也就自然引出了后面的问题:

有没有一种方式,既保留状态管理的价值,又把使用成本往下压一点?

于是,Pinia 出场了。


3. Pinia

3.1 背景

先看一张图:

image.png

这句话其实已经把定位说得很清楚了:

Vue 的官方状态管理库已经改为 Pinia。

也就是说,Pinia 并不是“Vue 生态里一个还不错的第三方库”,而是今天 Vue 官方推荐的状态管理方案。

从官方的描述来看,Pinia 很大程度上吸收了 Vuex 5 RFC 的设计方向,最终成为了 Vue 官方真正推进的那条路线。

image.png

顺带一提,Pinia 这个名字也挺有意思,它来源于西班牙语里的 Piña,也就是菠萝。

image.png

它的设计理念官方也写得很直白:

Type Safe, Extensible, and Modular by design. Forget you are even using a store.

翻成人话就是:

  • 类型友好;
  • 可扩展;
  • 天生模块化;
  • 最好让你用 store 的时候,感觉不到自己在“负重前行”。

image.png


3.2 Pinia 由什么组成?

Pinia 相比 Vuex,概念明显更少了,核心主要就是三部分:

  • state:状态本体;
  • getters:派生状态;
  • actions:业务操作和副作用逻辑。

你会发现这里少了一个 Vuex 时代非常熟悉的角色:

mutation。

这并不是说 Pinia 不再关心状态变更,而是它把“修改状态”这件事从外部接口层面简化掉了,不再要求你必须显式拆成 mutation + action 两层。

也正因为如此,Pinia 的整体使用体验会更贴近今天 Vue 3 的 Composition API 风格。


3.3 Pinia 主要是为了解决什么问题?

1)去掉 mutations,减少冗余分层

Vuex 时代,很多非常简单的逻辑也要分成两段:

mutations: {
  add(state, value) {
    state.sum += value
  }
},
actions: {
  addWait({ commit }, value) {
    setTimeout(() => commit('add', value), 500)
  }
}

这在小型逻辑里很容易让人产生一种感觉:

我明明只是想改个数,为什么还要先绕一圈。

Pinia 直接把操作收敛到 actions 里:

actions: {
  add(value) {
    this.sum += value
  },
  addWait(value) {
    setTimeout(() => this.add(value), 500)
  }
}

这样写并不意味着状态就变乱了,而是把原本人为拆开的层重新聚合了回来。

2)更贴合 Composition API

Vuex 在 Composition API 场景下虽然也能用,但你经常会写出这种代码:

const store = useStore()
const sum = computed(() => store.state.countModules.sum)
const bigSum = computed(() => store.getters['countModules/bigSum'])
const add = value => store.commit('countModules/add', value)

而 Pinia 的使用方式会更自然一些:

import { storeToRefs } from 'pinia'
import { useCountModulesStore } from '@/stores/countModules'

const store = useCountModulesStore()
const { sum, bigSum } = storeToRefs(store)

store.add(1)
store.addWait(1)

这套写法有两个很明显的优势:

  • 读状态时更像在用普通响应式对象;
  • 调方法时更像在调普通实例方法。

没有那么重的“框架仪式感”。

3)减少 magic strings,重构更稳

Vuex 在命名空间场景下,经常会出现:

  • 'countModules/add'
  • 'countModules/addWait'
  • 'countModules/bigSum'

这些字符串路径的问题在于:

  • 读起来不够直观;
  • 重构容易漏;
  • 自动补全也不够自然。

所以 Pinia 官方才会明确强调:

No more magic strings.

image.png

在 Pinia 里,你更常见到的是:

const counterStore = useCounterStore()
counterStore.add(1)
counterStore.addWait(1)

相比字符串路径,这种方式对 IDE、类型提示、重构替换都更友好。

4)扁平 store 结构,降低模块心智负担

Vuex 里,一个大 store 再套一层 modules,再叠 namespaced,再考虑动态注册,整个心智模型其实并不轻。

Pinia 的思路简单很多:

  • 每个 store 自己就是一个独立单元;
  • 不需要把所有 store 提前集中注册到一个巨大的对象里;
  • 用到哪个 store,就调用哪个 useXxxStore()

也就是说,在 Pinia 里:

  • stores/count.js 是一个 store;
  • stores/code.js 是一个 store;
  • stores/codex.js 还是一个 store。

天生就是扁平的,天然就带隔离。

5)降低 TypeScript 使用成本

这是我自己非常认可的一点。

很多时候,不是大家不想把状态管理写好,而是“类型成本”太高了。Vuex 的复杂模块、路径字符串、辅助映射和泛型配合起来,确实容易让 TypeScript 场景变得更重。

Pinia 在设计上更依赖类型推导,这就让 TS 的接入体验更自然,也更不容易逼着大家手写一堆额外类型封装。


3.4 Pinia 的使用

1)先把 pinia 挂到应用上

这一步非常直接:

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

Pinia 本质上就是一个全局 store 容器,createPinia() 负责创建它,然后通过 app.use() 注入到应用中。

2)定义 store

Pinia 提供两种主流写法:

  • Setup Store(Composition API 风格)
  • Option Store(Options API 风格)
Setup Store 写法
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const sum = ref(0)
  const bigSum = computed(() => sum.value * 10)

  function add(value) {
    sum.value += value
  }

  function addWait(value) {
    setTimeout(() => add(value), 500)
  }

  return { sum, bigSum, add, addWait }
})

这种写法最大的优点就是自然。你几乎就是在写一个普通的组合式函数,只不过这个函数返回的是一组全局可共享的状态和方法。

Option Store 写法
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    sum: 0
  }),
  getters: {
    bigSum: state => state.sum * 10
  },
  actions: {
    add(value) {
      this.sum += value
    },
    addWait(value) {
      setTimeout(() => this.add(value), 500)
    }
  }
})

如果你的项目团队更习惯 Options 风格,这种写法也完全没问题。

image.png

3)Pinia 很适合把副作用也收进 store 里

这个点我个人觉得很实用。

在 Pinia 的 setup store 里,你可以直接使用 Vue 的 watchwatchEffect 去监听 store 内部状态,然后把持久化、打点、联动请求这类副作用逻辑统一放到 store 里管理。

比如下面这个例子:

import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const sum = ref(0)
  const bigSum = computed(() => sum.value * 10)

  function add(value) {
    sum.value += value
  }

  watch(sum, newVal => {
    if (typeof window !== 'undefined') {
      localStorage.setItem('sum', String(newVal))
    }
  })

  return { sum, bigSum, add }
})

这意味着:

  • 监听逻辑不必散落在多个组件里;
  • 只要 store 被创建一次,这套副作用逻辑就会持续工作;
  • 所有使用这个 store 的组件都能共享同一份行为。

4)组件里怎么用?

使用方式也很直接:

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '../stores/counter'
import { useCodeStore } from '../stores/code'
import { useCodexStore } from '../stores/codex'

const counterStore = useCounterStore()
const codeStore = useCodeStore()
const codexStore = useCodexStore()

const { sum, bigSum } = storeToRefs(counterStore)
const { codeNum } = storeToRefs(codeStore)
const { codexNum } = storeToRefs(codexStore)

function addNumber() {
  counterStore.add(1)
}

function addNumberWait() {
  counterStore.addWait(1)
}
</script>

<template>
  <div>
    <h1>当前求和为:{{ sum }}</h1>
    <h1>当前大十倍:{{ bigSum }}</h1>
    <h1>codeModules 的代码行数为:{{ codeNum }}</h1>
    <h1>codexModules 的代码行数为:{{ codexNum }}</h1>

    <button @click="addNumber">+1</button>
    <button @click="addNumberWait">等 1 秒加 1</button>
  </div>
</template>

这里有个非常关键的 API:storeToRefs()

它的意义在于:

把 store 上的 state / getters 解构出来以后,仍然保持响应式。

这点和 Vuex + Composition API 里“直接解构会丢响应式”的体验相比,确实舒服很多。


4. Vuex 和 Pinia 到底怎么选?

聊到这里,其实可以给一个比较现实的结论了。

4.1 如果是新项目

我的建议很直接:优先用 Pinia。

原因也不复杂:

  • 它是 Vue 官方当前推荐方案;
  • 对 Vue 3 和 Composition API 更友好;
  • 语法更简洁,学习成本更低;
  • TypeScript 体验更自然;
  • 工程上更轻,维护起来也更顺手。

4.2 如果是老项目

如果你的项目本身还在 Vue 2 / Vuex 体系中稳定运行,那也没必要为了“追新”而硬切 Pinia。

毕竟迁移状态管理不是一个改导入路径就能解决的事,它牵涉到:

  • 现有模块组织方式;
  • 组件的调用方式;
  • 团队成员的使用习惯;
  • DevTools 调试方式;
  • 甚至测试代码都可能需要联动调整。

所以对于老项目,是否迁移更应该看收益比,而不是看流行度。

4.3 一个更重要的问题:不是所有状态都该进 store

这件事我想单独强调一下。

很多时候项目状态管理混乱,不是因为你没用 Vuex 或 Pinia,而是因为你把不该全局的东西也全局了

可以粗暴地这么理解:

更适合放全局 store 的

  • 用户信息
  • 权限信息
  • 主题配置
  • 国际化配置
  • 跨页面共享的数据
  • 多组件联动依赖的业务状态

更适合放组件内部的

  • 弹窗开关
  • 当前 tab 是否展开
  • 输入框内容
  • 某一块局部 UI 的临时状态

不要把 store 当作一个“什么都能塞”的大抽屉。抽屉塞久了,最后会变成一个谁都不敢碰的神秘黑盒。


5. 小结

到这里,Vue 全局状态管理这件事,我们就算完整地过了一遍。

回头看会发现,Vuex 和 Pinia 并不是谁把谁彻底推翻了,它们解决的核心问题其实是同一件事:

如何让共享状态变得可维护、可追踪、可协作。

只不过 Vuex 更强调“规矩”,而 Pinia 更强调“自然”。

  • Vuex 给你的是一套更强约束、更有仪式感的状态流转模型;
  • Pinia 给你的是一套更轻量、更贴合 Vue 3 心智的使用方式。

如果把它们比作交通规则,那 Vuex 有点像收费站,流程完整、检查严格;Pinia 更像 ETC,规则没少,但你过路时明显没那么折腾。

最后再总结几个我觉得最重要的观点:

  1. 全局状态管理是有必要的,但前提是你真的有“全局共享”的状态。
  2. 状态管理的本质不是把数据存起来,而是让数据修改有路径、出问题能追踪。
  3. Vuex 的核心价值在于建立规则,尤其适合帮助我们理解单向数据流和集中式状态管理。
  4. Pinia 则是在保留这些价值的基础上,把 API 和心智负担都做了一轮瘦身。
  5. 真正糟糕的不是你选 Vuex 还是 Pinia,而是项目里一边喊着状态管理,一边全篇乱改数据,最后把 store 用成大型缓存垃圾场。

所以归根结底,工具很重要,但比工具更重要的,是我们有没有想清楚:

这份状态到底应不应该共享,它应该由谁来改,它的变化路径是否足够清晰。

这件事想清楚了,Vuex 和 Pinia 都能成为好工具;想不清楚,再新的库也救不了一个到处偷改状态的项目。


6. 参考文档

facebookarchive.github.io/flux/?utm_s…

vuex.vuejs.org/

pinia.vuejs.org/

blog.isquaredsoftware.com/presentatio…

jelly.jd.com/article/634…