Skip to content

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编排异步任务或复杂业务,再提交 Mutationstore.dispatch('asyncAdd', 1)
Module按领域拆分 Store,可配置命名空间user/profile/load

官方 Getter 按 Vue 的 computed 组织,具有依赖追踪与缓存语义。 Getter 不是对 State 的监听器,而是响应式派生值。

Mutation 必须同步,是 Vuex 为保证状态变更可追踪而制定的规范, 不是 JavaScript 不能在异步回调中修改对象。 异步修改无法准确归属于 Mutation 的同步提交区间,会破坏调试链路。

核心执行流程 ​

初始化 ​

  1. createStore(options) 创建 Store 实例。
  2. 构造函数将根配置转换为模块集合。
  3. 初始化 Getter、Mutation、Action 等内部注册表。
  4. 递归安装根模块和子模块。
  5. 建立响应式 State,并创建 Getter 的计算值。
  6. 依次执行 plugins,每个插件接收 Store。
  7. 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) 的核心过程是:

  1. 根据 type 查找 Mutation 处理器数组。
  2. 进入内部提交区间,设置“正在提交”标记。
  3. 同步执行所有匹配处理器,并传入模块局部 State。
  4. 恢复此前的提交标记。
  5. 通知 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 4Pinia
Store 组织一个根 Store,通过 Module 拆分多个相互独立的 Store
核心概念State、Getter、Mutation、Action、ModuleState、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 } = counter

Action 可以直接解构,因为它会绑定 Store;State 和 Getter 不应直接普通解构。

如何选择 ​

  • 新建 Vue 3 项目:通常优先选择 Pinia,API 更精简,TypeScript 与组合式 API 体验更自然。
  • 维护现有 Vuex 项目:没有明确收益时不必为了追新而迁移,应评估模块数量、插件依赖和测试成本。
  • 团队强调严格变更流程:Vuex 的 Mutation 能形成显式事件边界,适合依赖该约束的项目。
  • 希望按业务拆分独立 Store:Pinia 的多 Store 模型通常更直观。

不要仅以“代码更少”决定是否迁移,也不要笼统声称 Pinia 的性能一定优于 Vuex; 选择应结合项目规模、存量架构、团队经验、插件生态与迁移成本。

总结 ​

  1. Vuex 4 通过 app.use(store) 安装,Store.install 执行 provide 注入。
  2. State 依靠 Vue 响应式系统更新视图,Mutation 提供可追踪的修改边界。
  3. 官方 Getter 按 computed 组织,具有缓存语义;教学版没有实现该缓存。
  4. Mutation 要求同步是追踪规范,不是 JavaScript 语言限制。
  5. 非严格模式直接改 State 仍可响应,但会绕过 Mutation、订阅和追踪。
  6. 严格模式同步深度监控 State,对提交区间外的修改发出警告。
  7. 官方 dispatch 单处理器返回其结果的 Promise 包装,多处理器才用 Promise.all。
  8. 模块 Action 接收局部 Context,包含局部与根级状态、Getter、提交和分发能力。
  9. 命名空间隔离类型;未命名空间模块的同名处理器可能被共同触发。
  10. 插件通过订阅扩展 Store;官方取消订阅等能力在教学版中可能简化。
  11. 教学版还简化了动态注册:采用重新安装方式,不代表完整官方实现。
  12. Pinia 是 Vue 官方推荐的新项目默认方案,但不是 Vuex 的官方升级版。