简介

以下内容结合官方文档

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

状态管理模式:

下面是单个组件的数据管理方式⬇

状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;(就是相当于组件内定义的data)
  • view,以声明方式将 state 映射到视图;(将数据呈现到视图上,便是view)
  • actions,响应在 view 上的用户输入导致的状态变化。(当用户点击某个操作使数据发生改变时)

下面是表示“单向数据流”理念的简单示意图

img

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

也就是说,当各自的组件都有各自的数据并不希望被别的组件访问时,可以使用自己的data属性去定义,去存储(自己的小房间),当多个组件都要共享一个状态(state)时,那么,你就要一个“状态管理管家”去帮你管理这些数据,这便是vuex

下面是官方图

基本使用

通过vuex管理一个加减器案例

首先就是用脚手架搭建我们的项目了,记得把vuex勾选上(忘记了也可以在后面npm安装上)

然后就是界面代码的书写,在app.vue中进行

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
31
32
33
<template>
<div id="app">
<h2>{{this.$store.state.count}}</h2>
<button @click="increase()">+</button>
<button @click="reduce()">-</button>
</div>
</template>

<script>
export default {
name: 'App',
data() {
return {
// message: this.$store.state.count //获取vuex中的状态
}
},
methods:{
increase() {
this.$store.commit("increase") //注意是字符串格式
},
reduce() {
this.$store.commit("reduce")
}
},
components: {
}
}
</script>

<style>

</style>

在store文件夹中,你会发现有一个index.js文件,它就是vuex状态管理的主文件,好的,让我们把它删了,重新创建一个,自己梳理并创建vuex

下面是vuex的基本创建代码

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

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

//新建实例
const Store = new Vuex.Store({
})

//导出store
export default Store

然后就是在新建的vuex实例中去定义我们的state和mutations,来为刚才的组件定义状态管理的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
//状态管理
state: {
count: 100
},
//行为管理
mutations: {
increase(state) {
state.count ++
},
reduce(state) {
state.count --
}
}

这里有一个注意点:虽然我们可以在组件中通过自定义方法(method)的方式去改变state的值,但是官方不推荐这样做,这样会破坏状态的统一管理(Vuex可以更明确的追踪状态的变化,所以不要直接在其它地方改变store.state中的值)

devtools

简介:vue-devtools是一款基于chrome游览器的插件,用于调试vue应用,这可以极大地提高我们的调试效率。

安装

  1. 有能力者可以直接上google chrome商店去安装该插件
  2. 极简插件按照安装指示中去安装该插件
  3. 通过npm

下面具体介绍通过npm

首先去到该插件的官方GitHub,然后下载该分支下的所选版本

image-20201027090026653

可以直接右边Code下载,有git的也可以在本地电脑上clone下载

然后解压(clone的可以直接跳过这一步),之后在该文件夹下执行npm i

安装完相应依赖后,执行npm run build

然后找到文件夹下的shell文件夹中的chrome,在chrome/edge中扩展该插件

注意:目前只能安装5.1.1版本(自己试的)

State单一状态树

Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

我们希望多个组件可以共享一个状态,如果我们在每个组件都定义的话,这会变得更加麻烦,并且,如何让它们共享动态改变的状态也是一个困难的点,所以vuex提供了store来为我们统一管理这些状态,通过store中的state属性,我们就可以很方便的管理这些状态了

但是:使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定

并且,正如其名,官方只推荐定义一个store去管理这些状态,否则后期维护会变得更加麻烦

Getters

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

store对象中定义getters

1
2
3
4
5
6
7
8
9
10
11
12
getters: {
//如果要定义参数,则要返回一个函数
//参数可以定义getters来访问其它getter
more20(state) {
let result = state.students.filter(n => n.age >= 20)
return result
// return function(a) {
// let result = state.students.filter(n => n.age >= 20)
// return result + a
// }
}
}

app.vue中使用

1
<h2>{{this.$store.getters.more20}}</h2>

如果想向getter传参数的话,则在getter里面返回一个函数

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//新建并导出Store对象
export default new Vuex.Store({
state: {
count: 100,
},
mutations: {
increase(state) {
state.count ++
},
reduce(state) {
state.count --
}
}
})

app.vue调用该方法

1
2
3
4
5
6
7
8
methods:{
increase() {
this.$store.commit("increase") //注意是字符串格式
},
reduce() {
this.$store.commit("reduce")
}
}

如果想自定义传递参数的话,可以这样做

1
2
3
4
5
6
methods:{
increase5() {
let data = 5
this.$store.commit("increase5", data)
}
}

在store中

1
2
3
4
5
6
7
8
9
10
11
12
//新建并导出Store对象
export default new Vuex.Store({
state: {
count: 100
},
mutations: {
//可以通过组件的commit('方法名',参数),来为状态管理中的方法传递参数
increase5(state,data) {
state.count += data
}
}
})

这个传入的data,实际为mutation的载荷,称作提交载荷(payload)

在大多数情况下,载荷最好是对象,这样可以包含多个字段并且记录的 mutation 会更易读:

1
2
3
4
5
mutations: {
increase5(state,payload) {
state.count += payload.data
}
}
1
2
3
4
5
6
methods:{
increase5() {
this.$store.commit("increase5", {
data: 5
})
}

提交风格

对象风格

1
2
3
4
5
6
7
increase5() {
let data = 5
this.$store.commit({
type: "increase5",
data
})
}

这里的data在mutation那里依旧会变成payload对象,mutation格式依旧不变

数据响应式原理

Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.

这就要求我们必须遵守一些Vuex对应的规则:

  • 提前在store中初始化好所需的属性.
  • 当给state中的对象添加新属性时, 使用下面的方式:
    1. 使用Vue.set(obj, key, value) (推荐)
    2. 用新对象给旧对象重新赋值

当我们想为state中的对象数据进行添加属性的时候,你可能会这样做

1
2
3
4
5
getters: {
changedata(state) {
state.students[0]['height'] = 1.88 //添加height属性
}
},

student的原有定义数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
state: {
count: 100,
students: [
{
name: 'a',
age: 15
},
{
name: 'b',
age: 20
},
{
name: 'c',
age: 30
}]
}

然后,给组件绑定这一事件,之后运行一下,你会发现,数据没有更新,准确的来说,应该是视图界面上的数据并没有更新,打开vue控制台,在vuex选项中你会发现,数据是更新了,但它就是不出来。

image-20201028103508241

如何解决这个问题,就是用上面的方法去增加属性

1
Vue.set(state.students[0],'height',1.88)

image-20201028103412118

类型常量

当我们给vuex添加mutation时,不可避免的,我们也需要到相对应的组件去使用这些方法,当方法越来越多时,我们就需要去将这些方法名一一对应,使得组件的方法名能和store对应起来,那么这个过程,我们就可能因为一两个看错而导致代码出错的问题,vue推荐我们使用类型常量去代替Mutation事件的类型,具体操作方法如下

  1. 在store文件夹中定义一个mutation-type.js文件去定义我们的常量

  2. 然后如下图所示,定义一个常量名,然后在组件和需要定义mutation文件中去引入该文件,之后就可以愉快的书写代码了,妈妈再也不用担心我的代码出错了,就算出错了,也有桥梁将它们联系起来,将错就错(嘿嘿)

    image-20201028105649059

mutation同步函数

通常情况下, Vuex要求我们Mutation中的方法必须是同步方法.

主要的原因是当我们使用devtools时, devtools可以帮助我们捕捉mutation的动作.

但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.

如果你在mutations定义了一个异步函数,那么,运行在devtools中你会发现,与这个异步操作关联的state中的数据并没有发生改变

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

他在定义上和mutation唯一的不同就是,它可以使用异步函数

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。

但是要注意的是:context并不就是store,后面在讲module就会说他们的区别

下面展示了在vuex中如何定义actions并在异步中调用mutation方法

1
2
3
4
5
6
7
8
9
10
11
12
mutations: {
increase5(state,payload) {
state.count += payload.data
},
},
actions: {
increase5(context,payload) {
setTimeout(() => {
context.commit('increase5',payload)
},1000)
}
}

在组件中的使用和mutation并不相同,它不是用commit()去调用该函数,而是通过dispatch()去调用该函数,同样,你可以给它传递参数,之后如上面代码所示,传递给mutations

1
2
3
4
5
6
7
increase5() {
let data = 5
this.$store.dispatch({
type: "increase5",
data
})
}

promise

同样的,我们可以利用学过的promise对其进行完善,从而确定异步函数是否正确执行

1
2
3
4
5
6
7
8
9
10
11
actions: {
// promise版本
increase5(context,payload) {
return new Promise((resolve) => {
setTimeout(() => {
context.commit('increase5',payload)
resolve('调用成功')
},1000)
})
}
},
1
2
3
4
5
6
7
8
9
10
11
12
13
increase5() {
let data = 5
this.$store.dispatch({
type: "increase5",
data
}).then(res => {
console.log(res)
let a = 'aaa'
return a
}).then(res => {
console.log(res)
})
},

Module

参照官网

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},

getters: {
doubleCount (state) {
return state.count * 2
}
}
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

1
2
3
4
5
6
7
8
9
10
const moduleA = {
// 这里使用了解构赋值,context.state context.commit context.rootState
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

1
2
3
4
5
6
7
8
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}

注意:虽然我们的一些mutation和action都是定义在module对象内部的,但是在调用的时候, 依然是通过this.$store来直接调用的.

项目结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到 action 里面。

只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块