一文带你彻底搞懂 Vue 全局状态管理:从 Vuex 到 Pinia
很多同学刚接触状态管理的时候,脑子里通常只有两个问题:
- 这玩意到底有没有必要?
- 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)跨组件共享状态非常麻烦
像 user、token、cart 这类数据,经常会在很多页面和组件里都要用到。
对于简单的父子组件,你还能靠 props 往下传;但一旦层级变深,问题就来了。你会发现自己开始做一件很机械、也很容易出错的事情:
为了给最底层组件传一个值,你要把中间整条链路都改一遍。
更离谱一点的项目,还会引入 event bus,到处 $emit / $on,最后组件之间像地下接头一样彼此通风报信。
2)状态修改没有约束,调试困难
很多项目在没有状态管理的时候,往往会出现这些写法:
window.userthis.$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,甚至很多轻量状态库都能看到它的影子。
Flux 里有四个经典角色:
- store:存储状态的地方;
- view:视图层,根据状态渲染页面;
- action:描述一次行为;
- dispatcher:负责把 action 分发出去。
3)单向数据流到底是什么意思?
很多文章都提“单向数据流”,但一讲就容易虚。
我的理解很简单:
应用里数据的产生、修改、再反馈到界面的路径,尽量只有一个固定方向。
放到 Vuex 里,可以理解成这样一条链路:
View(用户操作) → Action(表达意图) → Mutation(修改状态) → State 更新 → View 重新渲染
你可以把它想成一条高速公路:
车都按一个方向走,别有人在路上逆行。
一旦逆行,也就是多个地方绕过规则直接改状态,数据为什么错、视图为什么乱、逻辑为什么互相打架,这些问题就会一起冒出来。
4)Vuex 的核心组成
Vuex 在沿用 Flux 思路的基础上,结合 Vue 的特点做了一层封装。它主要由下面几部分组成:
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 提供了四个辅助函数:
mapStatemapGettersmapMutationsmapActions
比如下面这种写法,在 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 里,通常会更推荐:
computedtoReftoRefswatch
这些方式去处理响应式读取。
7)动态注册模块:能做,但心智不低
Vuex 还支持 registerModule 动态注册模块。
它适合一些按需加载场景,比如某个页面进入时才挂载模块,离开时再卸载。
if (!store.hasModule('codeModules')) {
store.registerModule('codeModules', codeModules)
}
这个能力本身不差,问题在于:
- 你要关心模块何时注册;
- 你要关心模块是否重复注册;
- 你还要考虑何时注销。
到了这里,Vuex 的“工程感”其实已经非常强了。它不是不能用,而是它开始要求开发者投入越来越多的心智来维护这套体系。
这也就自然引出了后面的问题:
有没有一种方式,既保留状态管理的价值,又把使用成本往下压一点?
于是,Pinia 出场了。
3. Pinia
3.1 背景
先看一张图:
这句话其实已经把定位说得很清楚了:
Vue 的官方状态管理库已经改为 Pinia。
也就是说,Pinia 并不是“Vue 生态里一个还不错的第三方库”,而是今天 Vue 官方推荐的状态管理方案。
从官方的描述来看,Pinia 很大程度上吸收了 Vuex 5 RFC 的设计方向,最终成为了 Vue 官方真正推进的那条路线。
顺带一提,Pinia 这个名字也挺有意思,它来源于西班牙语里的 Piña,也就是菠萝。
它的设计理念官方也写得很直白:
Type Safe, Extensible, and Modular by design. Forget you are even using a store.
翻成人话就是:
- 类型友好;
- 可扩展;
- 天生模块化;
- 最好让你用 store 的时候,感觉不到自己在“负重前行”。
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.
在 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 风格,这种写法也完全没问题。
3)Pinia 很适合把副作用也收进 store 里
这个点我个人觉得很实用。
在 Pinia 的 setup store 里,你可以直接使用 Vue 的 watch、watchEffect 去监听 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,规则没少,但你过路时明显没那么折腾。
最后再总结几个我觉得最重要的观点:
- 全局状态管理是有必要的,但前提是你真的有“全局共享”的状态。
- 状态管理的本质不是把数据存起来,而是让数据修改有路径、出问题能追踪。
- Vuex 的核心价值在于建立规则,尤其适合帮助我们理解单向数据流和集中式状态管理。
- Pinia 则是在保留这些价值的基础上,把 API 和心智负担都做了一轮瘦身。
- 真正糟糕的不是你选 Vuex 还是 Pinia,而是项目里一边喊着状态管理,一边全篇乱改数据,最后把 store 用成大型缓存垃圾场。
所以归根结底,工具很重要,但比工具更重要的,是我们有没有想清楚:
这份状态到底应不应该共享,它应该由谁来改,它的变化路径是否足够清晰。
这件事想清楚了,Vuex 和 Pinia 都能成为好工具;想不清楚,再新的库也救不了一个到处偷改状态的项目。
6. 参考文档
facebookarchive.github.io/flux/?utm_s…