讲 Vuex 之前,先说一下痛点。在复杂的 Vue 项目中,组件间的通信通常会演变成“面条代码”(错综复杂、逻辑混乱):
- Props 嵌套过深(Prop Drilling):爷爷传给孙子,中间隔了五层。
- 事件总线(Event Bus)混乱:到处是 $emit 和 $on,根本不知道是谁改了数据。
- 数据不一致:同一个用户信息,在 Header 改了,Sidebar 没变。
Vuex 的本质解决方案: 将分散在各处的“私有状态”抽离出来,建立一个全局单例模式的“中央仓库”
单向数据流的“闭环”
Vuex 并不是一个简单的全局变量,它强制执行一套**“可预测”**的变更规则。
- State:单一事实来源,响应式的数据。
- Getters:仓库的“计算属性”,具有缓存机制。
- Mutations:同步修改状态的唯一入口。为什么必须同步?为了让 DevTools 能捕获快照。
- Actions:处理异步逻辑(API 请求),最终必须 commit 一个 mutation。
Vuex 3 (Vue 2 时代)
Vuex 3 的核心是利用一个隐藏的 Vue 实例
内部实例化了一个隐藏的 Vue 实例。将 state 存放在该实例的 data 中,利用 Vue 2 的 Object.defineProperty 实现
// 源码简化逻辑
store._vm = new Vue({
data: {
$$state: state // 借用 Vue 的 data 实现响应式
},
computed: wrapperGetters // 把 getters 变成这个实例的计算属性
})
- Getter 实现:利用 Vue 实例的 computed。
- 注入:让 Vue 的每一个组件实例,都能在不需要显式 import 的情况下,访问到同一个 Store 实例
原理: 当你执行 Vue.use(Vuex) 时,Vuex 插件会在全局混入一个 beforeCreate 钩子。
分发逻辑:
1. 根组件(Root)在初始化时接收了 new Vuex.Store 实例。
2. 混入的钩子会检查当前组件:如果你有 store 选项,我就把它存到 this.$store。
3. 如果你没有(比如子组件),我就去this.$parent(父组件)那里拿它的 $store。
结果: 就像接水管一样,Store 沿着组件树一层层向下传递,最终所有组件的 this.$store 都指向了同一个内存地址
Vuex 4 (Vue 3 时代)
Vuex 4 抛弃了 Vue 实例,直接使用 Reactivity API。
import { reactive, computed } from 'vue'
this._state = reactive({ data: state })
- 注入方式:改用 Provide / Inject 机制,更加轻量
原理: 当你执行 app.use(store) 时,Vuex 4 内部调用了 app.provide('store', store)。
分发逻辑: Vue 3 引擎会在底层维护一个 provides 对象池。任何子组件通过 inject('store') 都能直接从这个池子里拿到引用。
作用: 这种方式更符合解耦原则。它不再强依赖于 $parent 这种父子链条,而是通过上下文(Context)直接获取,性能更高,也更安全。
Pinia
“Vuex 4 虽然适配了 Vue 3,但它依然背负着旧时代的‘包袱’:强制的 Mutation、繁琐的类型推导、以及那个无法拆分的单例大对象
在 Vuex 中,当你执行 new Vuex.Store({ modules: { ... } }) 时,底层发生了以下几件事:
- 单一状态树(Single State Tree):Vuex 的设计哲学要求全局只能有一个 Store 实例。所有的 Module 最终都会被递归地合并到这个主 Store 的 state 对象上。
- 注册即合并:即便你使用了 namespaced: true,Vuex 也只是在内部维护了一个复杂的路径映射(如 user/login),但它在内存中依然是一颗巨大的对象树。
- Tree-shaking 的死穴:因为所有的 Module 都在 new Vuex.Store 这个构造函数的大对象里。Webpack 或 Vite 看到的是一个整体引用的对象。即使你某个路由页面只用了 user 模块,没用 order 模块,打包工具也不敢把 order 删掉,因为它们都定义在同一个初始化脚本里
哪些数据适合放在vuex中?
用户身份与权限信息
几乎每个页面、每个组件都需要知道当前登录的是谁,以及他有没有权限操作某个按钮。
- 具体数据:Token、用户信息(头像、昵称、UID)、用户权限列表(Permissions)、角色(Roles)。
- 理由:这些数据在 App 启动时通过拦截器或路由守卫获取,随后在导航栏、个人中心、侧边栏等多个不相邻组件中共享
公共配置与环境偏好
影响整个应用表现的控制开关,通常由用户在“设置”中触发。
- 具体数据:主题模式:深色/浅色模式切换。
- 多语言(i18n):当前的语言选择(zh-CN / en-US)。
- 侧边栏状态:展开还是收起(影响多个布局组件的响应式计算)
复杂的跨组件通信数据
当两个组件在 DOM 树上相隔极远(比如 A 是 B 的曾孙组件的邻居),但它们又必须频繁同步状态时。
- 具体数据:购物车列表:点击底层的商品卡片,顶部的悬浮购物车图标数字要实时变。
- 全局播放器状态:音乐播放器的播放列表、进度、是否暂停。
- 消息中心:即时通讯(IM)中的未读消息计数。
Pinia
Pinia 彻底推翻了“单一实例”的物理结构,改成了**“工厂模式”**。
- 定义不代表挂载:当你写 export const useUserStore = defineStore(...) 时,这行代码只是定义了一个获取 Store 的函数。它本身不占用全局状态空间。
- 按需初始化:只有当你在某个组件里执行了 const user = useUserStore(),Pinia 才会去初始化这个特定的 Store。
- 天然支持 Tree-shaking:如果你在 A 页面 import 了 useUserStore,在 B 页面 import 了 useOrderStore。当用户只访问 A 页面时,打包工具发现 useOrderStore 没有被任何地方引用(或者可以被分包),它就可以安全地从当前包中剔除