Vuex 状态管理入门

Vuex 的核心概念

  • 官方定义:Vuex 是一个专为 Vue.js 应用程序开发的状态管理工具
  • 概念解析
    • 工具属性:可将其理解为 Vue 生态中的一个核心插件
    • 状态定义:在 Vuex 的语境中,状态数据
    • 核心功能:Vuex 的核心作用是帮助开发者管理应用中的数据
  • 管理的数据类型:Vuex 主要用于管理通用数据,即多组件共享的数据
    • 典型案例(购物车场景):在购物车页面中,商品数量的增减操作不仅影响当前商品列表的显示,还需要同步更新底部结算组件的总价,甚至在后续的确认订单页面中也需要调用该数据。此类跨组件频繁使用的数据,即为通用数据。

Vuex 的应用场景

Vuex 主要适用于以下两大核心场景,其本质特征是数据的通用性多组件共享

  1. 多组件读取同一状态
    • 某个状态需要在多个不同的组件中被读取和渲染。
    • 示例:用户登录后保存的个人信息。该信息可能同时需要在首页、商品详情页、订单页等多个组件中展示。虽然不一定频繁修改,但多处均需使用。
  2. 多组件共同维护同一状态
    • 多个组件不仅需要读取数据,还需要对该数据进行修改和更新。
    • 示例:即使只有少数几个组件需要共同修改和渲染同一份数据,为了避免复杂的组件间通信,也应将其交由 Vuex 统一管理。

Vuex 的核心优势

在引入 Vuex 之前,处理跨组件通用数据通常依赖传统的组件通信方式(如子传父、父传子)。这种方式在组件层级较深时,会导致数据传递链路过长,维护成本极高。Vuex 的引入解决了这一痛点,具备以下三大核心优势:

  1. 数据集中化管理
    • 将多组件共享的通用数据统一存储于 Vuex 的公共仓库(Store) 中。
    • 任何页面或组件均可直接从仓库中获取数据,实现了所有组件共同维护同一份数据,彻底消除了繁琐的层层传递。
  2. 响应式数据更新
    • Vuex 中的状态是响应式的。
    • 当仓库中的数据被某个组件修改后,所有引用了该数据的组件视图会立刻自动更新,极大提升了数据同步的效率与准确性。
  3. 操作流程简洁化
    • 组件无需关注复杂的层级关系,只需直接与 Vuex 仓库进行交互即可完成数据的读取与修改。
    • Vuex 提供了一系列辅助函数,能够有效简化代码操作,使数据交互逻辑更加清晰、快捷。

Vuex 状态管理入门小结

  1. 核心定义:Vuex 是一个状态管理工具(插件),主要用于管理应用中的通用数据(多组件共享数据)。
  2. 应用场景:适用于单一状态在多处使用,或多个组件共同维护同一份数据的场景,支持多处渲染与多处修改。
  3. 核心优势
    • 实现数据集中化管理
    • 具备响应式变化特性,数据更新自动驱动视图更新。
    • 操作流程简洁,借助辅助函数大幅降低代码复杂度。

Vuex 状态管理基础与环境搭建

为了深入理解 Vuex 的作用,需要构建一个多组件共享数据的典型业务场景。具体需求如下:

  1. 数据共享:根组件中定义一份初始数据(例如数值 100),该数据需要同时在子组件一(Son1)子组件二(Son2) 中进行渲染。
  2. 数据修改:子组件一提供加一操作,子组件二提供减一操作,任意组件均可对共享数据进行修改。
  3. 响应式更新:一旦任意组件修改了该数据,所有引用该数据的组件必须实现响应式同步更新

简而言之,该场景的核心目标是实现多个组件共享同一份数据,且任意组件均可修改该数据,并保证所有组件的数据状态实时同步

项目环境搭建步骤

基于 Vue CLI 脚手架创建项目,并搭建出上述多组件共享数据的基础静态环境。具体操作分为以下三个步骤:

创建 Vue 项目

打开终端,进入目标目录,执行以下命令创建项目:

1
vue create vuex-demo

在交互式配置界面中,按照以下标准进行选择:

  • 预设选择:选择自定义配置(Manually select features)。
  • 功能勾选:仅勾选 CSS 预处理器(CSS Preprocessors)。为保持基础学习阶段的目录简洁,暂不勾选 Router 和 Vuex(在后续实际项目中通常需要勾选,脚手架会自动完成相关配置)。
  • Vue 版本:选择 2.x
  • CSS 预处理器:选择 Less。
  • ESLint 配置:选择 ESLint + Standard config。
  • 代码检查时机:选择 Lint on save。
  • 配置文件存放位置:选择 In package.json(不生成独立的配置文件)。

等待脚手架自动完成项目创建与依赖安装。

清理与重构目录结构

项目创建完成后,进入项目目录并使用代码编辑器打开:

1
cd vuex-demo

为保持项目结构清晰,需对默认生成的目录和文件进行清理与重构:

  • 清理静态资源:删除 src/assets 目录下的默认图片资源。
  • 清理默认组件:删除 src/components 目录下的 HelloWorld.vue 文件。
  • 创建业务组件:在 src/components 目录下新建两个子组件文件,分别命名为 Son1.vueSon2.vue
  • 清理根组件:清空 src/App.vue 中的默认模板与逻辑代码,保留最基础的 Vue 组件结构。

编写组件静态代码

将预先准备好的静态样式与结构代码分别填充至对应的组件中。此阶段仅提供 UI 结构,不包含任何 JavaScript 业务逻辑。

App.vue(根组件)
在根组件中引入并渲染 Son1 和 Son2 两个子组件,并添加基础标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div id="app">
<h2>根组件 App</h2>
<Son1 />
<Son2 />
</div>
</template>

<script>
import Son1 from './components/Son1.vue'
import Son2 from './components/Son2.vue'

export default {
name: 'App',
components: {
Son1,
Son2
}
}
</script>

Son1.vue(子组件一)
提供基础的数据展示区域与加一操作按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="son1">
<h3>子组件一 (Son1)</h3>
<p>共享数据值:100</p>
<button>加一</button>
</div>
</template>

<script>
export default {
name: 'Son1'
}
</script>

Son2.vue(子组件二)
提供基础的数据展示区域与减一操作按钮。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div class="son2">
<h3>子组件二 (Son2)</h3>
<p>共享数据值:100</p>
<button>减一</button>
</div>
</template>

<script>
export default {
name: 'Son2'
}
</script>

运行与效果验证

完成上述代码编写后,在终端中启动开发服务器:

1
npm run serve

打开浏览器访问本地服务地址(通常为 http://localhost:8080)。此时页面应正确渲染出根组件及两个子组件的静态结构。

当前环境已具备父组件 App 嵌套子组件 Son1 和 Son2 的层级关系,且未包含任何数据交互逻辑。这为后续引入 Vuex 实现多组件数据共享提供了标准的测试环境。

Vuex 状态管理基础与环境搭建小结

完成了 Vuex 多组件数据共享环境的基础搭建。核心操作流程总结如下:

  1. 创建项目:使用 vue create 命令创建项目,选择 Vue 2.x 版本,仅勾选 CSS 预处理器,保持基础配置。
  2. 重构目录:清理默认生成的冗余文件,新建 Son1.vueSon2.vue 子组件。
  3. 填充代码:将静态 HTML 结构分别填充至 App.vueSon1.vueSon2.vue 中,完成纯静态页面的搭建。

多组件共享数据与空仓库初始化

在多组件共享同一份数据的业务场景下,需要有一个统一的位置来存放通用数据,以便任何组件都能从该位置获取所需数据。这个通用的数据存储位置通常被称为仓库(Store)

Vuex 版本选择规则

在使用 Vue 及其生态插件时,必须严格匹配 Vue 核心库与插件的版本。版本对应规则如下:

  • Vue 2 对应版本:Vue Router 3,Vuex 3(记忆口诀:233)。
  • Vue 3 对应版本:Vue Router 4,Vuex 4(记忆口诀:344)。

由于本教程基于 Vue 2 进行讲解,因此需要安装 Vuex 3 版本。

初始化 Vuex 仓库的核心步骤

初始化 Vuex 仓库共分为四个核心步骤:安装依赖新建模块文件创建并导出仓库导入与挂载

1. 安装 Vuex 插件

在终端中执行以下命令安装 Vuex 3 版本:

1
npm install vuex@3

注:如果在创建 Vue 项目时已勾选 Vuex 配置项,Vue CLI 脚手架会自动完成安装与基础配置,无需手动执行此步骤。若未勾选,则必须手动安装。

2. 新建模块文件

在项目的 src 目录下新建 store 文件夹,并在其中创建 index.js 文件。该文件将作为 Vuex 仓库的核心模块,用于存放所有与 Vuex 相关的核心代码。

目录结构如下:

1
2
3
src/
└── store/
└── index.js

3. 创建并导出仓库

src/store/index.js 文件中,需要完成插件的安装、仓库的创建以及模块的导出。

核心代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 导入 Vue 和 Vuex
import Vue from 'vue'
import Vuex from 'vuex'

// 2. 安装 Vuex 插件
Vue.use(Vuex)

// 3. 创建仓库实例
const store = new Vuex.Store({
// 此处暂为空仓库,后续将在此配置 state、mutations 等核心选项
})

// 4. 导出仓库实例
export default store

注:创建仓库时,使用的是挂载在 Vuex 对象上的 Store 构造函数(即 new Vuex.Store()),这与 Vue Router 的 new VueRouter() 语法略有不同,需注意区分。

4. 导入与挂载仓库

在项目的入口文件 src/main.js 中,导入刚才创建的 store 模块,并将其挂载到 Vue 根实例的配置项中。

核心代码实现

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import App from './App.vue'
// 导入 store 模块
import store from './store'

Vue.config.productionTip = false

new Vue({
// 挂载 store 到 Vue 根实例
store,
render: h => h(App),
}).$mount('#app')

验证仓库配置

完成上述步骤后,需要验证 Vuex 仓库是否已成功配置并全局挂载。

验证方法
在任意 Vue 组件的生命周期钩子(如 created)中,通过 this.$store 访问仓库实例并打印。

1
2
3
4
5
6
export default {
created() {
// 打印仓库实例以验证配置
console.log(this.$store)
}
}

验证结果分析

  • 如果控制台输出为 undefined,说明仓库未配置成功或未正确挂载(类似于未配置路由时打印 this.$router 的结果)。
  • 如果控制台成功输出 Store 实例对象,说明 Vuex 仓库已配置成功。此时,所有组件实例均可通过 this.$store 访问该通用数据对象。

Vuex 状态管理与数据流规范

Vuex State(状态)的概念与数据提供

在 Vuex 中,所有需要跨组件共享的数据统一存储在 Store 的 state(状态) 对象中。

State 与组件 Data 的区别

  • 组件 Data:属于组件自身的私有数据,仅在当前组件内部可用。
  • Store State:属于全局共享数据,应用中的所有组件均可访问和使用。

提供共享数据

若要在 Vuex 中提供共享数据,只需在实例化 new Vuex.Store 时,添加 state 配置对象,并在其中定义所需的变量。

1
2
3
4
5
6
7
8
9
10
11
12
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
// 添加 state 配置对象
state: {
title: '大标题',
count: 100
}
})

获取 State 数据的方案一:直接访问仓库

在提供数据后,组件需要获取并使用这些数据。第一种方案是直接通过 Store 实例进行访问。访问的核心逻辑为:先获取 Store 实例,再访问 state 对象,最后获取具体变量

根据代码所处的位置不同,获取 Store 实例的方式有所区别:

在组件模板(Template)中

在模板中访问 Store 无需使用 this,直接通过 $store 访问。

1
2
3
4
5
6
7
8
<template>
<div>
<!-- 访问 title -->
<h1>{{ $store.state.title }}</h1>
<!-- 访问 count -->
<p>当前计数:{{ $store.state.count }}</p>
</div>
</template>

在组件逻辑(Script)中

在组件的 methodscomputed 等逻辑选项中,需通过 this.$store 访问。

1
2
3
4
5
6
7
export default {
methods: {
printCount() {
console.log(this.$store.state.count)
}
}
}

在独立的 JS 模块中

在非 Vue 组件的普通 JS 文件(如 main.js 或独立的工具模块)中,需先通过 import 导入 Store 实例,然后直接访问。

1
2
3
4
import store from './store'

// 直接通过导入的 store 实例访问
console.log(store.state.count)

获取 State 数据的方案二:使用辅助函数 mapState

直接访问 $store.state.变量名 路径较长。若通过手动编写计算属性来简化模板中的调用,会导致代码冗余。Vuex 提供了 mapState 辅助函数,用于将 Store 中的 state 自动映射为组件的计算属性。

mapState 的核心作用

mapState 能够自动读取 Store 中的 state 数据,并将其转化为组件内部的计算属性,从而在模板中直接通过变量名进行调用。

使用步骤

使用 mapState 共分为三个核心步骤:

第一步:按需导入辅助函数
vuex 中导入 mapState

1
import { mapState } from 'vuex'

第二步:以数组形式调用
调用 mapState 时,传入一个数组,数组元素为需要映射的 state 属性名。该函数执行后会返回一个包含计算属性的对象。

1
mapState(['count', 'title'])

第三步:使用展开运算符(…)注入 computed
mapState 返回的对象通过 ES6 展开运算符(...)展开到组件的 computed 选项中。

1
2
3
4
5
6
7
8
9
10
11
export default {
computed: {
// 使用展开运算符,避免覆盖其他自定义计算属性
...mapState(['count', 'title']),

// 可以继续添加组件自身的计算属性
doubleCount() {
return this.count * 2
}
}
}

注意:必须使用展开运算符(...)。若直接赋值(如 computed: mapState(...)),会导致 mapState 返回的对象覆盖整个 computed 选项,使得组件无法再定义其他自定义计算属性。

模板中的简化调用

配置完成后,在模板中可直接使用映射后的变量名,无需再书写完整的 $store.state 路径。

1
2
3
4
5
6
<template>
<div>
<h1>{{ title }}</h1>
<p>当前计数:{{ count }}</p>
</div>
</template>

Vuex 单向数据流与严格模式

单向数据流原则

Vuex 严格遵循单向数据流原则。这意味着组件中不能直接修改仓库(Store)中的数据

错误示例

1
2
3
4
5
6
methods: {
addCount() {
// 错误写法:直接修改 state 数据
this.$store.state.count++
}
}

直接修改 State 会导致数据流向混乱,在大型项目中极难追踪数据变更的来源,严重降低代码的可维护性。正确的做法是向 Store 提交变更申请,由 Store 内部的 mutations 统一执行修改。

严格模式(Strict Mode)

由于检测直接修改 State 的行为需要消耗运行性能,Vue 默认不会对此类不规范操作进行报错。为了帮助开发者在开发阶段及时纠错,Vuex 提供了严格模式

开启方式
在实例化 Store 时,添加 strict: true 配置。

1
2
3
4
5
6
export default new Vuex.Store({
strict: true, // 开启严格模式
state: {
count: 100
}
})

严格模式的作用与注意事项

  • 作用:开启后,若在任何组件中直接修改了 State 数据,Vuex 会立即抛出错误(如 Do not mutate vuex store state outside mutation handlers),强制开发者遵循单向数据流。
  • 注意事项:严格模式会进行深度状态监听,消耗大量性能。因此,在生产环境(项目上线时)必须关闭严格模式,仅在开发环境中使用。

数据修改的正确规范

若需修改 State 数据,必须通过提交 mutations 来实现。所有对共享数据的修改操作都应集中在 Store 内部的 mutation 处理函数中执行,以确保数据流向清晰、状态变更可追踪。

Vuex Mutations 状态修改与传参机制

Mutations 的基本概念与核心原则

在 Vuex 状态管理中,State 中的数据不能直接修改。若开启严格模式,在组件内直接修改 State 数据会导致程序报错。所有对 State 数据的修改,必须且只能通过 Mutations 来进行。

Mutations 是 Vuex Store 配置项中的一个对象,用于存放一系列修改 State 的方法。该对象内的每一个方法即为一个 Mutation 函数,其核心作用是集中管理状态变更逻辑。

Mutations 的基本操作流程

修改 State 数据的标准流程分为两个核心步骤:

  1. 定义 Mutation 方法:在 Store 的 mutations 对象中定义方法。每个 Mutation 函数的第一个参数必须是 state,通过该参数访问并修改共享数据。
  2. 提交 Mutation:在组件中通过 this.$store.commit('方法名') 提交调用。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
state: {
count: 0,
title: '大标题'
},
mutations: {
// 第一个参数固定为 state
addCount(state) {
state.count += 1;
},
changeTitle(state) {
state.title = '小标题';
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 组件模板 -->
<template>
<div>
<p>当前数量:{{ $store.state.count }}</p>
<p>当前标题:{{ $store.state.title }}</p>

<button @click="handleAdd">加一</button>
<button @click="handleChangeTitle">修改标题</button>
</div>
</template>

<script>
export default {
methods: {
handleAdd() {
// 提交 mutation,方法名必须与定义时一致
this.$store.commit('addCount');
},
handleChangeTitle() {
this.$store.commit('changeTitle');
}
}
};
</script>

Mutations 的传参机制(提交载荷)

当多个操作属于同一类型(如加 1、加 5、加 10)时,为避免重复定义方法,可将其封装为一个带参数的 Mutation 函数,以提高代码复用性。

传参语法

  • 提交时传参this.$store.commit('mutationName', payload)
  • 接收参数:Mutation 函数的第二个参数用于接收传递的额外数据,该参数在 Vuex 中被称为提交载荷(Payload)

核心限制与多参数解决方案

提交载荷有且只能有一个参数。若业务场景需要传递多个数据,必须将其封装为对象数组进行传递。

1
2
3
4
5
6
7
8
9
10
// 正确做法:将多个参数封装为对象
this.$store.commit('addCount', { amount: 5, message: '增加5' });

// Mutation 中接收对象
mutations: {
addCount(state, payload) {
state.count += payload.amount;
console.log(payload.message);
}
}

实战案例:数值加减操作

通过提交载荷,可以将加法与减法操作分别复用为单一的 Mutation 方法。

Store 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/index.js
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
// 加法操作,接收参数 n
addCount(state, n) {
state.count += n;
},
// 减法操作,接收参数 n
subCount(state, n) {
state.count -= n;
}
}
});

组件调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>
<p>当前数量:{{ $store.state.count }}</p>

<!-- 加法按钮 -->
<button @click="handleAdd(1)">加一</button>
<button @click="handleAdd(5)">加五</button>
<button @click="handleAdd(10)">加十</button>

<!-- 减法按钮 -->
<button @click="handleSub(1)">减一</button>
<button @click="handleSub(5)">减五</button>
<button @click="handleSub(10)">减十</button>
</div>
</template>

<script>
export default {
methods: {
handleAdd(n) {
this.$store.commit('addCount', n);
},
handleSub(n) {
this.$store.commit('subCount', n);
}
}
};
</script>

实战案例:输入框双向数据绑定

Vuex 遵循单向数据流原则,禁止直接使用 v-model 绑定 State 数据(在严格模式下会直接报错)。为实现输入框与 State 的双向联动,需将 v-model 拆分为 :value@input

实现步骤

  1. 渲染数据:使用 :value 将 State 数据绑定到输入框。
  2. 监听输入:使用 @input 监听输入事件,获取最新的输入值。
  3. 封装 Mutation:定义带参数的 Mutation 方法用于更新 State。
  4. 提交更新:在事件处理函数中,通过 commit 提交 Mutation 并传递新值。

代码实现

1
2
3
4
5
6
7
8
9
10
11
// store/index.js
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
changeCount(state, newCount) {
state.count = newCount;
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 组件模板 -->
<template>
<div>
<p>当前数量:{{ count }}</p>
<!-- 禁用 v-model,使用 :value 和 @input 替代 -->
<input
type="number"
:value="count"
@input="handleInput"
/>
</div>
</template>

<script>
import { mapState } from 'vuex';

export default {
computed: {
...mapState(['count'])
},
methods: {
handleInput(e) {
// 获取输入框的值并转换为数字
const newValue = Number(e.target.value);
// 提交 mutation 更新状态
this.$store.commit('changeCount', newValue);
}
}
};
</script>

Vuex 辅助函数 mapMutations

概念与作用

在 Vuex 状态管理中,mapMutations 是一个用于简化组件与 Store 交互的辅助函数。

  • mapState 的作用是将 Store 中的状态(state)映射为组件的计算属性(computed)。
  • mapMutations 的作用是将 Store 中的突变方法(mutations)映射为组件的实例方法(methods)。

通过 mapMutations,开发者可以将 Store 中定义的 mutations 方法直接提取并映射到组件的 methods 选项中,从而避免在组件中频繁编写 this.$store.commit() 的冗余代码。

核心原理与等价代码

mapMutations 返回的是一个包含多个方法的对象。通过 ES6 的展开运算符(...),可以将这些方法直接混入到组件的 methods 中。

假设 Store 中定义了一个带参数的 mutation 方法 subCount

1
2
3
4
5
mutations: {
subCount(state, n) {
state.count -= n;
}
}

在组件中使用 mapMutations 进行映射:

1
2
3
methods: {
...mapMutations(['subCount'])
}

上述映射代码在底层完全等价于在 methods 中手动定义以下方法:

1
2
3
4
5
methods: {
subCount(n) {
this.$store.commit('subCount', n);
}
}

重点说明:映射生成的方法会自动处理参数传递。在组件中调用 this.subCount(n) 时,参数 n 会自动作为载荷(payload)传递给对应的 mutation。

基础语法与使用步骤

使用 mapMutations 的标准流程如下:

  1. vuex 模块中导入 mapMutations 辅助函数。
  2. 在组件的 methods 选项中,使用展开运算符 ... 展开 mapMutations 的返回值。
  3. 传入一个数组,数组元素为需要映射的 mutation 方法名(字符串格式)。
  4. 在组件的模板或逻辑中,直接通过方法名调用映射后的函数。

代码实战演示

映射带参数的减法操作

以减法功能为例,对应 Store 中的 subCount mutation。

组件代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<button @click="subCount(1)">减 1</button>
<button @click="subCount(5)">减 5</button>
<button @click="subCount(10)">减 10</button>
</div>
</template>

<script>
import { mapMutations } from 'vuex';

export default {
methods: {
// 映射 subCount 方法
...mapMutations(['subCount'])
}
}
</script>

执行逻辑

  • 页面加载后,组件的 methods 中自动注入了 subCount 方法。
  • 在模板中点击按钮时,直接调用 subCount 并传入对应参数。
  • 模板中调用映射方法时,无需添加 this 前缀,Vue 模板编译器会自动处理上下文。

映射修改标题操作

假设 Store 中提供了一个修改标题的 mutation 方法 changeTitle

组件代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<button @click="changeTitle('前端程序员')">修改标题</button>
</div>
</template>

<script>
import { mapMutations } from 'vuex';

export default {
methods: {
// 同时映射多个 mutation 方法
...mapMutations(['subCount', 'changeTitle'])
}
}
</script>

执行逻辑

  • 在配置数组中追加 'changeTitle',即可将修改标题的方法一并映射到当前组件。
  • 在模板的点击事件中,直接调用 changeTitle 并传入字符串参数 '前端程序员',即可完成状态修改。

总结与实践

  1. 职责区分mapState 用于映射状态到计算属性,mapMutations 用于映射方法到实例方法。
  2. 简化代码:利用辅助函数可以大幅减少 this.$store.commit 的样板代码,使组件逻辑更加清晰简洁。
  3. 模板调用:在 Vue 模板(template)中调用映射后的方法时,直接使用方法名即可,无需使用 this
  4. 参数传递:映射生成的方法完全支持参数传递,调用时传入的参数会自动作为载荷提交给 Store。

通过合理使用 mapMutations,可以有效提升 Vuex 状态管理的开发效率与代码可维护性。

Actions:处理异步操作

Actions

  • 专门用于处理异步操作
  • 不能直接修改 State,必须通过 commit 提交 Mutations 来修改。
  • 组件中通过 dispatch 触发,或使用 mapActions 映射到 methods 中简化调用。

Actions 的概念与作用

在 Vuex 中,Mutations 必须是同步函数。这一设计是为了便于 Vuex DevTools 进行状态快照、数据变化监测以及调试。如果直接在 Mutations 中执行异步操作(如 setTimeout 或发送网络请求),DevTools 将无法准确追踪状态的变化轨迹。

为了解决异步操作修改状态的需求,Vuex 引入了 Actions 概念。Actions 专门用于处理异步操作。需要注意的是,Actions 不能直接修改 State,它必须在异步操作完成后,通过提交(commit)Mutations 来间接修改 State。

Actions 的执行流程

涉及异步操作的状态修改流程如下:

  1. 在组件中通过 dispatch 触发 Action。
  2. Action 内部执行异步操作(如定时器、API 请求)。
  3. 异步操作完成后,Action 内部通过 commit 提交 Mutation。
  4. Mutation 同步修改 State,触发视图更新。

Actions 的定义与调用(原生方式)

在 Vuex 的 store 中定义 Actions,其方法的第一个参数为 context(上下文对象),第二个参数为 payload(额外载荷参数)。在未开启模块化的情况下,context 可以理解为 store 实例本身,因此可以通过 context.commit 来提交 Mutation。

Store 定义示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
count: 0
},
mutations: {
changeCount(state, payload) {
state.count = payload
}
},
actions: {
// 定义 Action,处理异步操作
changeCountAction(context, payload) {
// 使用 setTimeout 模拟异步操作(实际开发中多为网络请求)
setTimeout(() => {
// 异步操作完成后,提交 Mutation 修改 State
context.commit('changeCount', payload)
}, 1000)
}
}
})

组件调用示例:

在组件中,必须使用 this.$store.dispatch 来触发 Action,而不是 commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
<button @click="handleChange">一秒后修改为 666</button>
</div>
</template>

<script>
export default {
methods: {
handleChange() {
// 使用 dispatch 触发 Action,并传递载荷参数
this.$store.dispatch('changeCountAction', 666)
}
}
}
</script>

辅助函数 mapActions

与 Mutations 类似,Vuex 提供了 mapActions 辅助函数,用于将 Store 中的 Actions 映射为组件的 methods 方法,从而简化代码调用。

组件映射示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<!-- 直接调用映射后的方法 -->
<button @click="changeCountAction(888)">一秒后修改为 888</button>
</div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
methods: {
// 将 Actions 映射到组件的 methods 中
...mapActions(['changeCountAction'])
}
}
</script>

使用 mapActions 后,组件内可直接通过 this.changeCountAction(payload) 调用,其底层依然执行的是 this.$store.dispatch

Getters:派生状态(计算属性)

Getters

  • 相当于 Store 的计算属性,用于基于 State 派生新状态。
  • 具备响应式缓存特性,依赖的 State 变化时自动重新计算。
  • 只有 get 没有 set,属于只读属性。
  • 组件中通过 $store.getters 访问,或使用 mapGetters 映射到 computed 中简化调用。

Getters 的概念与作用

Getters 类似于 Vue 组件中的计算属性(computed)。当需要基于 Store 中的 State 派生出一些新的状态时(例如对数组进行过滤、计算数组长度等),应当使用 Getters。

Getters 的返回值会根据它的依赖(State)被缓存起来,且只有当它的依赖值发生改变时才会被重新计算。需要注意的是,Vuex 中的 Getters 只有 get 属性,没有 set 属性,即它是只读的。若要修改数据,仍需通过 Mutations。

Getters 的定义规则

在 Store 中定义 Getters 时,需遵循以下规则:

  1. 函数的第一个参数必须是 state,通过它可以访问 Store 中的状态。
  2. 函数必须有返回值,该返回值即为 Getters 计算后的结果。

Store 定义示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/index.js
export default new Vuex.Store({
state: {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
},
getters: {
// 定义 Getter,过滤出大于 5 的元素
filterList(state) {
return state.list.filter(item => item > 5)
},
// 定义 Getter,获取数组长度
listLength(state) {
return state.list.length
}
}
})

Getters 的访问与使用

在组件中访问 Getters 有两种方式:原生访问和辅助函数映射。

原生访问方式:

通过 this.$store.getters 对象进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>
<!-- 在模板中直接访问 -->
<p>大于5的元素:{{ $store.getters.filterList }}</p>
</div>
</template>

<script>
export default {
mounted() {
// 在 JS 逻辑中访问
console.log(this.$store.getters.filterList)
}
}
</script>

辅助函数 mapGetters

Vuex 提供了 mapGetters 辅助函数,用于将 Store 中的 Getters 映射为组件的 computed 计算属性。

注意区分映射位置:

  • mapStatemapGetters 映射的是属性,必须放在组件的 computed 中。
  • mapMutationsmapActions 映射的是方法,必须放在组件的 methods 中。

组件映射示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<!-- 直接使用映射后的计算属性 -->
<p>大于5的元素:{{ filterList }}</p>
<p>数组长度:{{ listLength }}</p>
</div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
computed: {
// 将 Getters 映射到组件的 computed 中
...mapGetters(['filterList', 'listLength'])
}
}
</script>

通过 Vue DevTools 可以观察到,当 State 中的 list 数据发生变化时(如新增或修改元素),Getters 中的 filterListlistLength 会自动重新计算并更新视图,具备完整的响应式特性。

Vuex 进阶核心概念:模块(Module)

单一状态树与大型项目的维护痛点

单一状态树的概念

Vuex 使用单一状态树(Single State Tree),即用一个唯一的对象(state)来包含全部的应用层级状态。在常规配置中,开发者会在 state 对象中添加任意数量的属性,例如与个人信息相关的 userInfo、与主题色相关的 theme、与网站描述相关的 desc,以及与购物车相关的 cart 等。由于所有的状态都集中在一个对象上,该对象被称为单一状态树。

大型项目中的维护痛点

当应用逻辑变得复杂、项目规模逐渐增大时,需要往仓库(Store)中存储的数据会急剧增加。这会导致以下问题:

  • Store 对象臃肿:所有的状态数据都集中在一个 state 对象中,导致对象体积庞大。
  • 代码高度耦合:所有与数据相关的 mutationsactions 都会集中在同一个 JavaScript 文件中。
  • 可维护性降低:随着代码量的增加,单一文件会变得难以阅读和维护,开发者在修改特定业务逻辑时,需要在庞大的文件中反复查找。

模块化解决方案

模块化的核心思想

为了解决单一状态树在大型项目中的维护难题,Vuex 允许将 Store 分割成多个模块(Module)。每个模块都可以拥有自己独立的 statemutationsactions 以及 getters

模块化的优势

通过将庞大的 Store 拆分为多个独立的小模块(如用户模块、设置模块、购物车模块等),可以实现以下优势:

  • 逻辑隔离:每个模块只负责管理自身相关的状态和逻辑,模块之间保持独立。
  • 分类管理:根级别的状态与子模块的状态分类管理,互不干扰。
  • 提升可维护性:开发者只需关注当前业务对应的模块文件,无需在单一文件中维护所有代码,显著提高了中大型项目的可维护性。

模块的创建与注册步骤

创建模块目录与文件

在项目的 store 目录下,新建一个名为 modules 的文件夹,用于存放所有的子模块文件。根据业务需求,在该文件夹下创建对应的模块文件,例如:

  • user.js:用于管理用户相关状态。
  • setting.js:用于管理应用设置相关状态。

定义模块内部的核心概念

在每个模块文件中,需要分别定义该模块专属的 statemutationsactionsgetters,并将它们整合为一个配置对象进行导出。

用户模块(user.js)代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const state = {
userInfo: {
name: '张三',
age: 18
},
score: 80
}

const mutations = {}
const actions = {}
const getters = {}

export default {
state,
mutations,
actions,
getters
}

设置模块(setting.js)代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const state = {
theme: 'light',
desc: '测试Demo描述'
}

const mutations = {}
const actions = {}
const getters = {}

export default {
state,
mutations,
actions,
getters
}

在根 Store 中导入并注册模块

创建好子模块后,必须在 store/index.js 核心文件中导入这些模块,并通过 Vuex 的第五大核心概念 modules 选项进行挂载注册。

根 Store(index.js)代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import setting from './modules/setting'

Vue.use(Vuex)

export default new Vuex.Store({
state: {
title: '根级别标题',
count: 0,
list: []
},
mutations: {},
actions: {},
modules: {
user,
setting
}
})

在上述代码中,modules 对象内的属性名即为子模块的名称,属性值为导入的模块配置对象。

模块的验证与调试

完成模块的创建与注册后,可以通过 Vue Devtools 调试工具来验证配置是否成功。

  1. 打开浏览器的 Vue Devtools 面板,选择 Vuex 选项卡。
  2. 刷新页面后,观察状态树的结构。
  3. 如果配置成功,除了根级别(root)的状态(如 titlecountlist)之外,还能看到以子节点形式挂载的 usersetting 子模块。
  4. 点击对应的子模块,可以查看该模块内部独立维护的 statemutations 等详细信息。

从本质上讲,Vuex 依然保持着单一状态树的底层数据结构,但在代码组织和文件结构层面,已经成功实现了模块化拆分。

模块总结

Vuex 的第五大核心概念:模块(Module)。掌握模块化语法,是构建和维护中大型 Vue 应用程序的必要技能。

  • 单一状态树的局限:在大型项目中,将所有状态集中在根级别的 index.js 文件中会导致代码臃肿、难以维护。
  • 模块化的实现:通过将 Store 拆分到不同的模块文件中,每个模块拥有各自独立的 statemutationsactionsgetters
  • 模块的注册:最终只需将导出的模块对象在根 Store 中导入,并注册到 modules 配置项中,即可完成模块的创建与应用。

Vuex 模块化状态管理

本节系统讲解 Vue2 环境下 Vuex 模块化(Modules)状态管理的核心机制。内容涵盖子模块中 stategettersmutationsactions 的访问与调用方法,并提供原生写法与辅助函数映射两种标准方案。

模块中的 State 访问

理论基础:单一状态树

尽管 Vuex 采用了模块化设计,但其本质依然遵循单一状态树原则。子模块的状态(state)最终会被挂载到根级别的 state 对象中。这种设计保证了状态结构的唯一性,同时通过模块划分大幅提升了代码的可维护性。

方案一:原生方式访问

通过原生方式访问子模块状态时,需要从根级别的 $store.state 出发,逐层向下查找。

访问语法

1
this.$store.state.模块名.状态名

代码示例

1
2
3
4
5
// 访问 user 模块下的 userInfo.name
this.$store.state.user.userInfo.name

// 访问 setting 模块下的 theme
this.$store.state.setting.theme

注意事项:在编写原生访问路径时,必须严格遵循 state -> 模块名 -> 具体状态 的层级顺序,初学者易将 state模块名 的顺序混淆。

方案二:辅助函数 mapState 映射

使用 mapState 辅助函数可以将 store 中的 state 映射为组件的计算属性,从而简化模板中的调用。

映射根级别状态

若直接映射根级别状态,会将整个模块对象映射为计算属性:

1
2
3
4
5
6
7
import { mapState } from 'vuex';

export default {
computed: {
...mapState(['user', 'setting'])
}
}

在模板中使用时,仍需通过 user.userInfo.name 进行深层访问。

映射子模块状态(需开启命名空间)

为了直接映射子模块内部的具体状态,必须在该模块的配置中开启命名空间namespaced: true)。

模块配置

1
2
3
4
5
6
7
// user.js
export default {
namespaced: true, // 开启命名空间
state: {
userInfo: { name: '张三' }
}
}

组件映射

1
2
3
4
5
6
7
8
9
import { mapState } from 'vuex';

export default {
computed: {
// 第一个参数为模块名,第二个参数为需要映射的状态数组
...mapState('user', ['userInfo']),
...mapState('setting', ['theme', 'desc'])
}
}

开启命名空间后,模块拥有独立的作用域,辅助函数在映射时会自动限定在指定模块内,使代码更加清晰简洁。

模块中的 Getters 访问

理论基础:Getters 的存储特性

state 不同,getters 在默认情况下并不会按模块层级嵌套存储,而是直接挂载到全局的 $store.getters 对象中。其键名由模块名getter名通过斜杠(/)拼接而成,包含特殊字符。

子模块 Getters 定义

1
2
3
4
5
6
7
8
9
10
11
// user.js
export default {
namespaced: true,
state: { userInfo: { name: '张三' } },
getters: {
// 分模块后,state 参数指代当前子模块的 state
upperCaseName(state) {
return state.userInfo.name.toUpperCase();
}
}
}
方案一:原生方式访问

由于全局 getters 对象的键名包含斜杠(如 user/upperCaseName),在 JavaScript 中无法使用点语法(.)直接访问,必须使用中括号([] 结合字符串进行访问。

访问语法

1
this.$store.getters['模块名/getter名']

代码示例

1
2
// 访问 user 模块的 upperCaseName
this.$store.getters['user/upperCaseName']
方案二:辅助函数 mapGetters 映射

mapState 类似,使用 mapGetters 映射子模块数据同样需要开启命名空间

代码示例

1
2
3
4
5
6
7
import { mapGetters } from 'vuex';

export default {
computed: {
...mapGetters('user', ['upperCaseName'])
}
}

在模板中可直接使用 {{ upperCaseName }} 进行渲染,避免了原生写法中繁琐的中括号语法。

模块中的 Mutations 调用

理论基础:命名空间的重要性

默认情况下,子模块的 mutationsactions 会被挂载到全局。若不开启命名空间,调用时无需指定模块名,这会导致方法来源不明确,严重降低代码的可维护性。因此,强烈建议为所有子模块开启命名空间,使其挂载到对应的子模块作用域中。

方案一:原生方式调用

开启命名空间后,使用 $store.commit 提交 mutation 时,第一个参数必须包含模块名前缀。

访问语法

1
this.$store.commit('模块名/mutation名', payload)

模块定义

1
2
3
4
5
6
7
8
9
// user.js
export default {
namespaced: true,
mutations: {
setUser(state, newUserInfo) {
state.userInfo = newUserInfo;
}
}
}

组件调用

1
2
3
4
5
methods: {
updateUserInfo() {
this.$store.commit('user/setUser', { name: '李四', age: 25 });
}
}

方案二:辅助函数 mapMutations 映射

使用 mapMutations 可以将模块内的 mutation 映射为组件的 methods。

代码示例

1
2
3
4
5
6
7
8
import { mapMutations } from 'vuex';

export default {
methods: {
...mapMutations('user', ['setUser']),
...mapMutations('setting', ['setTheme'])
}
}

映射完成后,在组件内可直接通过 this.setUser(payload) 调用,或在模板中通过 @click="setUser(payload)" 触发。

模块中的 Actions 调用

理论基础:Actions 与上下文对象

actions 的调用机制与 mutations 高度一致,同样推荐开启命名空间。
需要特别注意的是,在 actions 内部调用 mutations 时,接收到的第一个参数 context当前模块的上下文对象,而非全局 store。因此,在 context.commit 时,无需再次添加模块名前缀

模块定义

1
2
3
4
5
6
7
8
9
10
11
12
// user.js
export default {
namespaced: true,
actions: {
setUserSecond(context, newUserInfo) {
setTimeout(() => {
// context 指向当前模块,直接提交 mutation 名称即可
context.commit('setUser', newUserInfo);
}, 1000);
}
}
}

方案一:原生方式调用

在组件中触发带有命名空间的 action 时,需使用 $store.dispatch,并指定完整的模块路径。

访问语法

1
this.$store.dispatch('模块名/action名', payload)

代码示例

1
2
3
4
5
methods: {
updateUserInfoAsync() {
this.$store.dispatch('user/setUserSecond', { name: '王五', age: 28 });
}
}

方案二:辅助函数 mapActions 映射

使用 mapActions 辅助函数可以简化 action 的调用逻辑。

代码示例

1
2
3
4
5
6
7
import { mapActions } from 'vuex';

export default {
methods: {
...mapActions('user', ['setUserSecond'])
}
}

在模板或方法中直接调用 this.setUserSecond(payload) 即可触发异步操作。

总结与最佳实践

  1. 单一状态树原则:无论模块如何划分,state 始终遵循单一状态树结构,原生访问时需严格遵循 $store.state.模块名.状态名 的层级。
  2. 强制开启命名空间:为避免全局命名冲突并提升代码可维护性,所有子模块必须配置 namespaced: true
  3. Getters 的特殊访问:原生访问全局 getters 时,因键名包含斜杠,必须使用中括号语法 $store.getters['模块名/getter名']
  4. 优先使用辅助函数:在组件中频繁使用 store 数据或方法时,优先使用 mapStatemapGettersmapMutationsmapActions,并传入模块名作为第一个参数,以大幅简化代码结构。
  5. Action 上下文机制:在 action 内部使用 context.commit 提交 mutation 时,系统会自动识别当前模块作用域,无需重复书写模块名前缀。