Appearance
Vuex 4 核心原理
导航目录
概述
Vuex 4 是面向 Vue 3 的集中式状态管理库。 它把跨组件共享状态放入 Store,并约定统一的读取、派生、修改与异步流程。 这种约束让状态变化更容易定位、订阅、记录和调试。
Vue 3 应先创建应用,再通过 app.use(store) 安装 Store:
js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
app.mount('#app')app.use(store) 会调用 Store.install(app, injectKey)。 该方法执行 app.provide(...) 注入 Store,供 useStore() 获取; 官方实现也会设置 app.config.globalProperties.$store,方便选项式 API 使用。
本文以 Vuex 4 官方机制为主线,并结合仓库教学实现解释核心原理。 教学代码用于理解流程,不等同于官方源码,也没有覆盖全部 API 和边界情况。
五大核心概念
| 概念 | 作用 | 典型用法 |
|---|---|---|
| State | 保存共享状态,整体表现为单一状态树 | store.state.count |
| Getter | 从 State 派生数据,具有计算属性语义 | store.getters.double |
| Mutation | 同步描述并执行状态修改 | store.commit('add', 1) |
| Action | 编排异步任务或复杂业务,再提交 Mutation | store.dispatch('asyncAdd', 1) |
| Module | 按领域拆分 Store,可配置命名空间 | user/profile/load |
官方 Getter 按 Vue 的 computed 组织,具有依赖追踪与缓存语义。 Getter 不是对 State 的监听器,而是响应式派生值。
Mutation 必须同步,是 Vuex 为保证状态变更可追踪而制定的规范, 不是 JavaScript 不能在异步回调中修改对象。 异步修改无法准确归属于 Mutation 的同步提交区间,会破坏调试链路。
核心执行流程
初始化
createStore(options)创建Store实例。- 构造函数将根配置转换为模块集合。
- 初始化 Getter、Mutation、Action 等内部注册表。
- 递归安装根模块和子模块。
- 建立响应式 State,并创建 Getter 的计算值。
- 依次执行
plugins,每个插件接收 Store。 app.use(store)触发Store.install,完成provide注入。
仓库教学实现的构造顺序与上述主线相近: 先创建 ModuleCollection,再执行 installModule 和 resetStoreState, 最后初始化订阅列表并运行插件。
模块注册
Vuex 先把原始模块配置转换成模块树。 模块节点保存原始配置、局部 State、子模块和 namespaced 标记。 安装模块时会:
- 根据模块路径定位局部 State;
- 将非根模块的 State 挂到父级 State;
- 根据路径计算命名空间;
- 注册 Getter、Mutation 和 Action 包装器;
- 递归安装所有子模块。
未启用命名空间时,不同模块的同名 Mutation 或 Action 可注册到同一类型下; 提交或分发该类型时,所有匹配处理器都会执行。
官方 registerModule 支持运行时注册,并配套状态保留、注销和热更新等能力。 仓库教学实现注册后会从根模块重新安装并重置状态, 这是便于展示流程的“重装”简化,可能重复收集处理器,动态注册并不完整。
响应式
官方实现将根 State 放入 Vue 响应式容器, store.state 通过访问器暴露其中的数据。 组件在渲染或计算属性中读取 State 后,相关变化会触发视图更新。
响应式与 Mutation 规范是两个维度:
- 响应式系统负责在数据变化后更新依赖;
- Mutation 负责标记变化来源并维持可追踪性。
因此,非严格模式下直接执行 store.state.count++ 仍然会响应更新, 但它绕过 Mutation,也绕过基于 Mutation 的订阅、日志和调试追踪。 直接修改 State 不应成为常规写法。
Getter
安装模块时,官方将 Getter 包装并收集到内部注册表。 重置 Store 状态时,每个包装 Getter 由 computed 承载, 再通过 store.getters[key] 对外暴露其值。
模块 Getter 的完整参数形态为:
js
getters: {
total(localState, localGetters, rootState, rootGetters) {
return localState.value + rootState.base
}
}局部 Getter 与根 Getter 最终进入统一注册体系,命名空间决定公开键名。
仓库教学实现只通过 Object.defineProperty 在访问时直接执行 Getter, 没有使用 computed,因此不具备官方 Getter 的缓存实现, 不能据此误解 Vuex 4 官方行为。
Commit
store.commit(type, payload) 的核心过程是:
- 根据
type查找 Mutation 处理器数组。 - 进入内部提交区间,设置“正在提交”标记。
- 同步执行所有匹配处理器,并传入模块局部 State。
- 恢复此前的提交标记。
- 通知 Mutation 订阅者,传入描述对象与最新 State。
官方内部提交包装会保存并恢复旧标记,便于嵌套调用。 Mutation 中启动异步任务并不违反 JavaScript 语法, 但回调执行时已离开提交区间,变化无法准确归因。
Dispatch
store.dispatch(type, payload) 根据类型查找 Action 处理器。 官方行为需要按处理器数量区分:
- 单个处理器:返回该处理器结果的 Promise 包装;
- 多个处理器:通过
Promise.all聚合结果; - 抛错或返回拒绝 Promise:
dispatch返回拒绝的 Promise。
模块 Action 接收局部 Context,而不是整个 Store:
js
actions: {
async load(
{ state, getters, commit, dispatch, rootState, rootGetters },
payload
) {
if (getters.ready) return state.data
await dispatch('prepare', null, { root: true })
commit('setData', payload)
return rootGetters['config/version']
}
}其中:
state、getters指当前模块;commit、dispatch默认在当前命名空间解析类型;rootState、rootGetters用于访问根级数据;{ root: true }可让提交或分发从根命名空间解析。
仓库教学实现把整个 Store 直接传给 Action,未构造局部 Context; 其 dispatch 无论处理器数量都执行 Promise.all, 所以单处理器的结果也会成为数组。 这两点都是教学简化,不是 Vuex 4 官方行为。
简化伪代码
以下伪代码是对 Vuex 4 官方机制的结构性描述,用于建立心智模型; 它不是官方源码的逐行复制,也不代表仓库教学实现的全部细节。
js
class Store {
constructor(options) {
this.modules = buildModuleTree(options)
this.mutations = createRegistry()
this.actions = createRegistry()
this.wrappedGetters = createRegistry()
installModule(this, [], this.modules.root)
resetStoreState(this, this.modules.root.state)
options.plugins?.forEach(plugin => plugin(this))
}
install(app, key = storeKey) {
app.provide(key, this)
app.config.globalProperties.$store = this
}
commit(type, payload) {
const handlers = this.mutations[type]
withCommit(this, () => handlers?.forEach(fn => fn(payload)))
notifyMutationSubscribers(this, { type, payload })
}
dispatch(type, payload) {
const handlers = this.actions[type]
if (!handlers?.length) return
const result = handlers.length > 1
? Promise.all(handlers.map(fn => fn(payload)))
: handlers[0](payload)
return Promise.resolve(result)
}
}
function resetStoreState(store, state) {
store.stateContainer = reactive({ data: state })
store.getters = exposeComputedGetters(store.wrappedGetters)
if (store.strict) enableSynchronousDeepWatch(store)
}关键问题
为什么不能在 Mutation 中处理异步逻辑
“不能”指不符合 Vuex 约定,而非语法上无法执行。Mutation 的进入和退出构成同步追踪区间;延迟回调执行时该区间已经结束,开发工具无法可靠标注变化来源。应在 Action 中等待异步任务,再同步 commit。
为什么 Action 不直接修改 State
Action 负责流程编排,Mutation 是状态变更边界。统一经 Mutation 修改,订阅器、日志和开发工具才能观察到一致的事件序列。
直接修改 State 会怎样
非严格模式下,响应式数据仍会变化,视图通常也会更新;但此次修改没有 Mutation 事件,会绕过 Mutation 订阅、日志与追踪。严格模式通过同步深度监控检测此类修改并发出警告。
Getter 与普通方法有什么区别
官方 Getter 基于 computed,依赖不变时复用缓存结果;普通方法每次调用都会重新执行。若 Getter 返回接收参数的函数,缓存的是函数本身,函数内部按参数完成的查询不会自动按参数缓存。
模块命名空间
namespaced: true 会把该模块路径加入 Getter、Mutation 和 Action 的类型。 例如模块 account/profile 中的 load 可形成:
js
store.dispatch('account/profile/load', payload)
store.commit('account/profile/update', payload)命名空间按路径逐层累积,但只有声明 namespaced: true 的模块贡献片段。 未开启命名空间的模块会把处理器注册到继承而来的命名空间中。
组件可使用 mapState、mapGetters、mapMutations、mapActions, 也可借助 createNamespacedHelpers('account/profile') 减少重复前缀。
在命名空间 Action 内,局部 commit 和 dispatch 默认补全当前前缀; 访问根处理器时传入 { root: true },访问根数据则使用 Context 中的根字段。
仓库教学实现能够拼接命名空间, 但没有实现官方局部 Context,因此不能完整演示局部与根级解析规则。
严格模式
配置 strict: true 后,Vuex 会同步、深度监控 State。 状态发生变化时,如果当前不在内部提交区间,就发出警告。
js
const store = createStore({
strict: import.meta.env.DEV,
state: () => ({ count: 0 }),
mutations: {
increment(state) {
state.count++
}
}
})严格模式的目标是发现非法修改,不是赋予 State 响应式能力。 同步深度监控存在运行成本,通常只在开发环境开启。
仓库教学实现使用 watch,并设置 deep: true、flush: 'sync', 再以内部提交标记判断修改是否合法,体现了该机制的核心思想。
插件与订阅
插件是接收 Store 的函数,适合实现持久化、日志、审计等横切能力。
js
function persistPlugin(store) {
store.subscribe((mutation, state) => {
sessionStorage.setItem('VUEX_STATE', JSON.stringify(state))
})
}
const store = createStore({ plugins: [persistPlugin] })store.subscribe 在每次 Mutation 完成后接收 Mutation 描述和最新 State。 官方 subscribe 返回取消订阅函数,并支持订阅选项; subscribeAction 则可观察 Action 的前置、后置和错误阶段。
仓库示例插件会读取会话存储、调用 replaceState 恢复数据, 并在 Mutation 后持久化 State。 其订阅实现只保存并调用回调,没有返回取消函数; 订阅取消、Action 订阅等 API 可能被简化或未实现。
插件不应直接依赖教学实现的私有字段,业务代码也应优先使用公开 API。
最佳实践
- 按业务领域拆分模块,不按组件机械拆分。
- State 保持最小化,可派生数据放入 Getter。
- Mutation 保持同步、短小,并准确表达变更意图。
- 异步请求、流程组合和条件判断放在 Action 中。
- 使用常量或清晰命名降低类型字符串的拼写风险。
- 为大型模块启用命名空间,避免全局类型冲突。
- 组件只读取所需状态,避免对整棵 State 做昂贵监听。
- 严格模式通常仅在开发环境开启。
- 持久化时选择必要字段,并处理版本迁移与解析异常。
- 不在业务代码中使用
_actions、_mutations等私有字段。 - 动态模块离开业务作用域时,应考虑注销和资源清理。
- TypeScript 项目可封装类型化的访问入口,减少字符串调用错误。
Vuex 4 与 Pinia 对比
Vuex 4 与 Pinia 都能管理 Vue 应用中的共享状态,但二者的设计方式不同: Vuex 4 强调单一状态树和明确的 Mutation 提交流程;Pinia 将状态拆分为多个独立 Store,并移除了 Mutation。
Pinia 不是“Vuex 5”或“Vuex 的升级版本”,而是另一套状态管理库。目前 Vue 官方更推荐新项目使用 Pinia。
核心差异
| 维度 | Vuex 4 | Pinia |
|---|---|---|
| Store 组织 | 一个根 Store,通过 Module 拆分 | 多个相互独立的 Store |
| 核心概念 | State、Getter、Mutation、Action、Module | State、Getter、Action |
| 同步修改 | 通常通过 commit 提交 Mutation | 可直接修改,也可使用 $patch 或 Action |
| 异步处理 | 在 Action 中执行,再提交 Mutation | 在 Action 中直接处理和修改 State |
| 模块隔离 | 依赖 Module 和 namespaced | 每个 Store 通过唯一 ID 天然隔离 |
| TypeScript | 支持,但常需额外类型封装 | 类型推导通常更自然 |
| 组合式 API | 可通过 useStore() 使用 | 同时支持选项式和 Setup Store |
| 解构响应式 | 通常使用 computed 或映射辅助函数 | 使用 storeToRefs() 保持响应式 |
| 开发工具 | 支持状态快照和 Mutation/Action 调试 | 支持时间线、Action 调试和状态编辑 |
| 适用场景 | 既有 Vuex 项目、依赖 Mutation 规范的团队 | Vue 3 新项目、偏好组合式 API 的团队 |
状态修改方式
Vuex 4 使用 Mutation 作为同步状态变更边界:
js
// Vuex 4
const store = createStore({
state: () => ({ count: 0 }),
mutations: {
increment(state, step = 1) {
state.count += step
}
},
actions: {
async incrementAsync({ commit }, step) {
await request()
commit('increment', step)
}
}
})
store.commit('increment', 2)
await store.dispatch('incrementAsync', 2)Pinia 不再区分 Mutation 和 Action。同步修改可以直接完成,复杂逻辑通常放入 Action:
js
// Pinia
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment(step = 1) {
this.count += step
},
async incrementAsync(step) {
await request()
this.count += step
}
}
})
const counter = useCounterStore()
counter.increment(2)
await counter.incrementAsync(2)Pinia 还可以直接修改 State,或通过 $patch 批量更新:
js
counter.count++
counter.$patch({
count: counter.count + 1
})
counter.$patch(state => {
state.count++
})模块化方式
Vuex 4 在根 Store 中注册 Module。启用 namespaced: true 后,调用时需要携带命名空间:
js
const store = createStore({
modules: {
user: {
namespaced: true,
state: () => ({ profile: null }),
mutations: {
setProfile(state, profile) {
state.profile = profile
}
}
}
}
})
store.commit('user/setProfile', profile)Pinia 通常按业务领域定义多个 Store,不需要嵌套 Module 或手动维护命名空间:
js
export const useUserStore = defineStore('user', {
state: () => ({ profile: null }),
actions: {
setProfile(profile) {
this.profile = profile
}
}
})一个 Pinia Store 可以直接调用另一个 Store,从而组合业务能力:
js
export const useCartStore = defineStore('cart', {
actions: {
checkout() {
const userStore = useUserStore()
if (!userStore.profile) return
// 提交订单
}
}
})组件中使用
Vuex 4 通常通过 useStore() 获取根 Store,并用 computed 保持派生引用:
js
const store = useStore()
const count = computed(() => store.state.count)
const double = computed(() => store.getters.double)Pinia 可以直接读取 Store,但解构 State 或 Getter 时应使用 storeToRefs(),避免丢失响应式:
js
const counter = useCounterStore()
const { count, double } = storeToRefs(counter)
const { increment } = counterAction 可以直接解构,因为它会绑定 Store;State 和 Getter 不应直接普通解构。
如何选择
- 新建 Vue 3 项目:通常优先选择 Pinia,API 更精简,TypeScript 与组合式 API 体验更自然。
- 维护现有 Vuex 项目:没有明确收益时不必为了追新而迁移,应评估模块数量、插件依赖和测试成本。
- 团队强调严格变更流程:Vuex 的 Mutation 能形成显式事件边界,适合依赖该约束的项目。
- 希望按业务拆分独立 Store:Pinia 的多 Store 模型通常更直观。
不要仅以“代码更少”决定是否迁移,也不要笼统声称 Pinia 的性能一定优于 Vuex; 选择应结合项目规模、存量架构、团队经验、插件生态与迁移成本。
总结
- Vuex 4 通过
app.use(store)安装,Store.install执行provide注入。 - State 依靠 Vue 响应式系统更新视图,Mutation 提供可追踪的修改边界。
- 官方 Getter 按
computed组织,具有缓存语义;教学版没有实现该缓存。 - Mutation 要求同步是追踪规范,不是 JavaScript 语言限制。
- 非严格模式直接改 State 仍可响应,但会绕过 Mutation、订阅和追踪。
- 严格模式同步深度监控 State,对提交区间外的修改发出警告。
- 官方
dispatch单处理器返回其结果的 Promise 包装,多处理器才用Promise.all。 - 模块 Action 接收局部 Context,包含局部与根级状态、Getter、提交和分发能力。
- 命名空间隔离类型;未命名空间模块的同名处理器可能被共同触发。
- 插件通过订阅扩展 Store;官方取消订阅等能力在教学版中可能简化。
- 教学版还简化了动态注册:采用重新安装方式,不代表完整官方实现。
- Pinia 是 Vue 官方推荐的新项目默认方案,但不是 Vuex 的官方升级版。