Vuex学习

📚 Vue 学习目录

🚀 Vue.js基础学习 - 📝 Vue.js进阶 - 🔦 webpack学习

📦 Vue-CLI学习 - 📌 Vue-router学习 - 🔮 Vuex学习 - 🎀 Nuxt.js 学习

什么是Vuex

Vuex就是一个状态管理模式,为什么叫模式?

因为Vuex包含了一套对state(状态)的操作规范,集中存储管理应用的所有组件的状态。

状态管理

  • 简单来说就是管理各个组件共享的数据,类似session

  • session可以存数据,存的过程就是管理,数据的每一次赋值就是当次状态。

  • Vuex在Vue实例顶层中。

    Vuex也可以理解为java中的一个map,这个map是static(静态资源)的,每次获取map的值,都需要调用java的api,比如map.get(key)获取对应的值,也可以放入数据map.put(data),而且这个数据是所有类都可以调用的,只需要导入这个map就能使用里面的共享数据。

    不了解java也没关系,你也可以理解成为百度百科就是一个容纳百科知识的容器,你要搜vuex,百科就会出现vuex的描述,这个搜索就是获取状态,如果你角色百科的vuex描述有误。你也可以发起修改操作,改成正确的vuex描述,这个修改操作就是修改vuex在百科里面的状态。当然你可以搜索修改vuex,别人也可以,因为这个状态是共享的。

    简单来看实现这个功能好像我们自己封装一个对象就能实现,但是Vuex有一个特性就是响应式。如果我们自己封装对象想做到完美响应式比较麻烦,所有Vuex帮我们做了这个事情。

什么状态需要Vuex去管理?

  • 比如用户的登录的状态(token)、用户的信息(头像、名称、地理位置信息)等等
  • 比如商品的收藏,购物车的商品等等
  • 这些状态应该是响应式的,用户昵称、头像修改了需要响应

Vuex简单模型

vuex简单模型

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

这是一个单页面数据流向,比如想要修改用户昵称,当前用户昵称的状态是A,通过输入框输入了新的昵称B,调用ajax请求后端修改成功后将state改成B,然后视图响应用户昵称的变化从A到B。

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

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。
  • 所以我们需要vuex的规范操作来管理状态。

Vuex基本使用

传统方法使用父子组件数据传递

使用Vue-CLI4创建项目

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
<template>
<div id="app">
<h3>{{ message }}</h3>
<h3>{{ count }}</h3>
<button @click="count++">+</button>
<button @click="count--">-</button>
<hello-vuex :count="count"/>
</div>
</template>

<script>
import HelloVuex from './components/HelloVuex'

export default {
name: 'App',
data () {
return {
message: '',
count: 0
}
},
components: {
HelloVuex
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="hello">
<h2>{{ message }}</h2>
<h2>{{ count }}</h2>
</div>
</template>

<script>
export default {
name: 'HelloVuex',
data () {
return {
message: 'HelloVuex'
}
},
props: {
count: Number
}
}
</script>

此时我们使用了父子组件通信来完成子组件HelloVuex获取父组件的count

如果不是父子组件如何通信,此时就需要vuex了,还是这2个组件,现在不使用父子通信,直接使用vuex。

使用vuex管理状态

  • 使用npm install vuex --save安装Vuex

  • 安装插件Vue.use(Vuex),在src下新建一个store文件夹,新建一个index.js

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

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

    // 2.创建对象
    const store = new Vuex.Store({
    state: { // 状态集合
    count: 0 // 具体的状态数据
    }
    })

    // 3.导出store对象
    export default store
  • 修改App.vueHelloVuex.vue,直接使用$store.state.count获取count值

    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
    <template>
    <div id="app">
    <h3>{{ message }}</h3>
    <h3>{{ $store.state.count }}</h3>
    <button @click="$store.state.count++">+</button>
    <button @click="$store.state.count--">-</button>
    <hello-vuex />
    </div>
    </template>

    <script>
    import HelloVuex from './components/HelloVuex'

    export default {
    name: 'App',
    data () {
    return {
    message: ''
    }
    },
    components: {
    HelloVuex
    }
    }
    </script>

    HelloVuex.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <template>
    <div class="hello">
    <h2>{{ message }}</h2>
    <h2>这是HelloVuex的count:{{ $store.state.count }}</h2>
    </div>
    </template>

    <script>
    export default {
    name: 'HelloVuex',
    data () {
    return {
    message: 'HelloVuex'
    }
    }
    }
    </script>

    一般不会直接使用$store.state.count获取vuex中的状态,也不是直接使用$store.state.count++来操作vuex中的状态。

Vuex的流程

状态管理图例

  • Vue Components是vue组件
  • Mutations :更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
  • State 是vuex中状态的集合
  • Actions与Mutations 类似,经常与后端交互,不同在于:
    • Action 提交的是 mutation,而不是直接变更状态。
    • Action 可以包含任意异步操作。

想要更好的调试Vue项目可以下载Chrome插件 vue-devtools

组件中修改 state,通过提交 mutation,修改完成后vuex帮我们响应到vue组件上。

案例:

  • 修改index.js使用mutation

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //创建对象
    const store = new Vuex.Store({
    state: { // 状态集合
    count: 200 // 具体的状态数据
    },
    mutations: { // 操作修改 state(状态) 默认有一个 state 参数
    increment (state) { // 增加
    state.count++
    },
    decrement (state) { // 减少
    state.count--
    }
    }
    })
  • 修改App.vue提交mutation

    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
    34
    <template>
    <div id="app">
    <h3>{{ message }}</h3>
    <h3>{{ $store.state.count }}</h3>
    <button @click="add">+</button>
    <button @click="sub">-</button>
    <hello-vuex />
    </div>
    </template>

    <script>
    import HelloVuex from './components/HelloVuex'

    export default {
    name: 'App',
    data () {
    return {
    message: ''
    }
    },
    components: {
    HelloVuex
    },
    methods: {
    add () {
    // 通过commit内填入mutations里的方法
    this.$store.commit('increment')
    },
    sub () {
    this.$store.commit('decrement')
    }
    }
    }
    </script>
  • 通过vue-devtools测试

    vue-devtools测试

​ 测试发现没有问题与直接使用$store.state.count++效果一致,通过提交mutation修改了状态state,在vue-devtools中也能跟踪state变化以及提交的mutation。

Vuex的核心概念

  • State
  • Getters
  • Mutation
  • Action
  • Moudule

State(单一状态树)

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

简单说就是把数据所有有关的数据封装到一个对象中,这个对象就是store实例,无论是数据的状态(state),以及对数据的操作(mutation、action)等都在store实例中,便于管理维护操作。

state直接通过this.$store.state获取state对象。

1
2
3
4
5
const store = new Vuex.Store({
state: {

}
})
mapstate辅助函数

当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:

store/index.js

1
2
3
4
5
6
const store = new Vuex.Store({
state: {
name: "zykj",
count: 200
}
})

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
34
35
36
37
38
<template>
<div id="app">
{{ name }}
{{ countAlias }}
{{ countPlusLocalState }}
</div>
</template>

<script>
//在使用mapState之前,要导入这个辅助函数
import {mapState} from "vuex";

export default {
name: 'App',
data() {
return {
localCount: 200
}
},
components: {},
computed: mapState({
// 箭头函数可使代码更简练
name: (state) => state.name,

// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',

// 为了能够使用 this 获取局部状态,必须使用常规函数
countPlusLocalState(state) {
return state.count + this.localCount
}
})
}
</script>

<style>

</style>

Getters

Getters类似计算属性,帮我们做一些重复的事情。

案例

有这样一个store实例,我们需要获取年龄大于20岁的学生数量:

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: { // 状态集合
students: [
{id: 110, name: 'zzz', age: '18'},
{id: 111, name: 'ttt', age: '20'},
{id: 112, name: 'yyy', age: '22'},
{id: 113, name: 'zty', age: '25'}
]
}
})

你可能会这样写这样一个计算属性去获取年龄大于20岁的学生数量:

1
2
3
4
5
computed: {
stuCount() {
return this.$store.state.students.filter(student => student.age > 20).length
}
}

如果很多组件中需要年龄大于20岁的学生数量,你可能会将这个计算属性复制,将filter函数写很多遍,但是如果你有Getters。

  • 在store实例中定义getters

    1
    2
    3
    4
    5
    getters: {
    getStudentCounts: state => {
    return state.students.filter(s => s.age > 20).length
    }
    }
  • 通过属性调用getters

    现在只需要调用getters的getStudents对象,就能获取数量。

    1
    2
    3
    4
    5
    computed: {
    stuCount() {
    return this.$store.getters.getStudents
    }
    }

如果你想查询指定ID(传入ID)的学生信息。

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

  • 定义getters

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //写法1
    getters: {
    getStuById: state => id => {
    return state.students.find(s => s.id === id)
    }
    }

    //写法2
    getters: {
    getStuById(state){
    return function(age){
    return state.students.find(s => s.id === id)
    }
    }
    }
  • 通过方法访问

    1
    2
    3
    4
    5
    computed: {
    stuById() {
    return this.$store.getters.getStuById(110)
    }
    }

    传入学生ID为110,输出就可以获得结构

Vuex允许在 store 中定义”getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 是响应式的,只有当数据发送改变才会重新计算。

Getter的参数:state 作为其第一个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
state: {
data: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneData: state => {
return state.data.filter(todo => todo.done)
}
}
})
通过属性访问

Getter 会暴露为 store.getters 对象,以属性的形式访问这些值:

1
store.getters.doneData // -> [{ id: 1, text: '...', done: true }]

Getter可以将 getter 作为第二个参数:

1
2
3
4
5
6
getters: {
doneDataCount: (state, getters) => {
return getters.doneData.length
}
}
store.getters.doneTodosCount // -> 1

我们可以很容易地在任何组件中使用它:

1
2
3
4
5
computed: {
doneDataCount () {
return this.$store.getters.doneDataCount
}
}

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

通过方法访问

通过 getter 返回一个函数,实现 getter 传参。在你对 store 里的数组进行查询时非常有用。

1
2
3
4
5
6
7
8
getters: {
getDataById(state){
return function(id){
return state.data.find(todo => todo.id === id)
}
}
}
store.getters.getDataById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

mapGetters 辅助函数

自己理解:不需要每次在计算属性中返回 actions

通过import { mapGetters } from 'vuex'导入

如:我们想获取数组的长度

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
data: [1,2,3,4]
},
getters: {
dataLength(state){
return state.data.length
}
}
})

在组件中使用

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
<template>
<div id="app">
{{ dataLength }}
</div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
name: 'App',
data () {
return {
}
},
computed: {
// 数组形式
...mapGetters(['dataLength'])
// 对象形式
//...mapGetters({
// // 把 `this.length` 映射为 `this.$store.getters.dataLength`
// length: 'dataLength'
// })
}
}
</script>

Mutation(状态更新)

状态管理图例

  • Vuex的store状态更新的唯一方式:提交Mutation

  • Mutation主要包括两个部分:

    1. 字符串的事件类型(type)
    2. 一个回调函数(handler),这个回调函数就是我们实际进行状态更改的地方,该回调函数的第一个参数就是state
  • Mutation的定义方式

    1
    2
    3
    4
    5
    mutation: {
    increment (state) {
    state.count++
    }
    }
  • 通过Mutation更新(修改state里的数据)

    1
    2
    3
    4
    5
    methods: {
    mutation() {
    this.$store.commit('increment')
    }
    }
Mutation 接受单个参数

mutation携带的参数被称为是mutation的载荷(Payload)

案例

在Vuex流程的案例上修改,添加2个按钮分别是让state中的count+5count+10,增加2个按钮

1
2
<button @click="addCount(5)">+5</button>
<button @click="addCount(10)">+10</button>

新增addCount方法

1
2
3
addCount (count) {
this.$store.commit('addCount', count) // 将count传入
}

新增一个mutation

1
2
3
4
5
mutation: {
addCount (state, count) { // 第二个参数是count,第一个始终是state
state.count += count
}
}
Mutation 接受多个参数

如果mutation需要接受多个参数,此时可以传一个对象

案例

例如新增一个功能点击按钮新增一个学生,此时需要传学生的ID、姓名、年龄,可以封装成一个学生对象传入。

  1. 新增按钮

    1
    <button @click="addStu()">新增一个指定的学生</button>
  2. 新增mutation

    1
    2
    3
    4
    addStu (state, stu) {
    state.students.push(stu) // 向数组中添加指定的stu
    console.log(state.students.find(s => s.id === stu.id)) // 输出打印查看state中是否有新增stu
    }
  3. 新增(methods)方法

    1
    2
    3
    4
    addStu () {
    const stu = { id: 114, name: 'zykj', age: '35'}
    this.$store.commit('addStu', stu)
    }
Mutation 的提交风格
  1. 普通提交风格

    1
    this.$store.commot('increment', count)

    此时count传过去的就是count=10

  2. 对象方式提交

    1
    2
    3
    4
    this.$store.commit({
    type: 'addCount',
    count
    })
    1
    2
    3
    4
    addCount (state, payload) { // 此时传入的就不是一个count值了,而是一个对象
    //获取时也是通过对象
    state.count += payload.count
    }

    此时count传过去是一个对象payload(载荷)

    1
    2
    3
    4
    {
    type: 'incrementCount',
    count: 10
    }
Vuex的响应式原理
  1. Vuex的store的state是响应式的,当state中的数据发生改变时,Vue组件会自动更新。
  2. 响应式需要遵循规则
    • state的对象需要初始化
    • 如果需要给state中的对象添加新属性的时候,使用以下方式
      1. 使用Vue.set(obj, 'newProp', 123)
      2. 用新对象替换旧对象
查看示例
  1. 在state中增加一个对象user

    1
    2
    3
    4
    user: {
    name: 'zhangsan',
    sex: '男'
    }
  2. 在app.vue增加按钮修改信息

    1
    2
    <h3>{{ $store.state.user }}</h3>
    <button @click="updateInfo()">修改信息</button>
  3. app.vue增加按updateInfo()方法

    1
    2
    3
    updateInfo () {
    this.$store.commit('updateInfo', 12)
    }
  4. 在mutation中添加updateInfo()

    1
    2
    3
    updateInfo (state, age) {
    state.user.age = age
    }
  5. 点击修改信息按钮,发现state的值变化了,但是页面没有响应变化(是因为直接修改数据不是相应式的、需要使用其他方法)

  6. 使用Vue.set()方法支持响应式

    1
    2
    3
    4
    updateInfo (state, age) {
    // state.user.age = age
    Vue.set(state.user, 'age', 12)
    }
  7. 再次点击修改信息按钮,发现变响应式了

总结

  1. state未初始化属性(age

    • 使用直接赋值的方式不能响应式
    • 需要使用 Vue.set(state.user, 'age', 12)
  2. state已经初始化了,可以使用直接赋值方式

  3. 关于删除属性

    1
    2
    3
    // 该方法没有响应式,需要使用Vue.delete
    // delete state.user.age
    Vue.delete(state.user, age)// 响应式删除age
Mutation 的类型常量

一个vue文件中有关mutation的方法太多了,常常可能写错,所有可以在store文件夹下定义一个mutation-type.js的常量。

  1. 定义一个mutation-type.js的常量、并将其导出

    1
    2
    3
    4
    updateInfo (state, age) {
    // state.user.age = age
    Vue.set(state.user, 'age', 12)
    }
  2. 修改App.vue的updateinfo方法

    1
    2
    3
    4
    5
    6
    //导入变量
    import { UPDATEINFO } from './store/mutation-type'

    [UPDATEINFO] () {
    this.$store.commit(UPDATEINFO, 12)
    }
  3. 修改store的index.js,将mutation的方法名也改成常量使用方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import { UPDATEINFO } from './mutation-type'

    [UPDATEINFO] (state, age) {
    // state.user.age = age
    Vue.set(state.user, 'age', 12)
    // 该方法没有响应式,需要使用Vue.delete
    // delete state.user.age
    // Vue.delete(state.user, age)// 响应式删除age
    }

    这样保证了所有的方法都定义在mutation-type.js中,不会出问题。

Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

查看示例

使用mutation操作更新state的时候,使用异步修改数据。

修改updateInfo()方法

1
2
3
4
5
6
[UPDATEINFO] (state, age) {
Vue.set(state.user, 'age', 12)
setTimeout(() => { // 延时模拟异步网络请求
state.user.name = 'lisi'
}, 1000)
}

发现页面的数据改变了,但是vue-devtools工具中并未跟踪到改变。所以我们不要在mutation中进行异步操作。这里就引出了我们之后要学习的 Action

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。简单来说就是在mutation上使用异步函数会导致devtools不能追踪

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作
查看示例
  1. 新增一个mutation

    1
    2
    3
    updateName (state, name) {
    state.user.name = name
    }
  2. 新增一个actions:第一个参数默认为 context (上下文),第二个参数为 传入的参数

    1
    2
    3
    4
    5
    6
    7
    8
    actions: {
    // context:上下文
    aUpdateInfo (context, name) {
    setTimeout(() => {
    context.commit('updateName', 'zykj')
    }, 1000)
    }
    }
  3. App.vue中新增一个按钮修改user对象姓名

    1
    2
    <h3>异步修改的信息:{{ $store.state.user }}</h3>
    <button @click="aUpdateInfo()">异步修改信息</button>
  4. 给按钮新增方法

    1
    2
    3
    4
    aUpdateInfo () {
    // 第一个参数为actions中的函数、第二个为参数
    this.$store.dispatch('aUpdateInfo', 'lisi')
    }
  5. 点击异步修改信息按钮测试

    在点击按钮之后,信息修改了,dev-tools也能跟踪到state的变化。通过$store.dispacth()方法来调用actions,发送异步请求,在actions中需要提交mutation来修改state。

  6. actions回调,在异步操作后,成功或者失败都应该会有回调$store.dispacth()返回一个Promise对象,修改actions,返回一个Promise对象,成功调用resolve(msg),将成功的msg传入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    actions: {
    // context:上下文
    aUpdateInfo (context, name) {
    let msg = '响应成功'
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    context.commit('updateName', 'zykj')
    resolve(msg)
    }, 1000)
    })
    }
    }
  7. 修改aUpdateInfo()方法,获取回调参数msg,此时的response就是actions中回调的msg,也可以支持失败的回调,只要actions中使用了reject,在aUpdateInfo()方法中catch回调结果就能获取resjct对象回传结果。

    1
    2
    3
    4
    5
    aUpdateInfo () {
    this.$store.dispatch('aUpdateInfo', 'lisi').then(response => {
    console.log(response)
    })
    }
  8. 再次点击异步修改信息,打印结果信息

    Actions 支持同样的载荷方式(payload)和对象方式进行分发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 以载荷形式分发
    store.dispatch('aUpdateInfo', {
    name: 'lisi'
    })

    // 以对象形式分发
    store.dispatch({
    type: 'aUpdateInfo',
    name: 'lisi'
    })
分发 Action

Action 通过 store.dispatch 方法触发:

1
store.dispatch('mutation中的函数名')
组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

1
2
3
4
5
6
7
8
9
10
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

现在你可以:

1
2
3
store.dispatch('actionA').then(() => {
// ...
})

在另外一个 action 中也可以:

1
2
3
4
5
6
7
8
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

moudules(模块)

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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 的状
模块的局部状态
  1. 模块内部的mutation 和 getter,接收的第一个参数是模块的局部状态对象(state)

  2. 模块内部的 action,局部状态是 context.state ,根节点状态则为 context.rootState

  3. 对于模块内部的 getter,第三个参数是根节点状态(rootState)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const moduleA = {
    state: () => ({
    count: 0
    }),
    mutations: {
    increment (state) {
    // 这里的 state 对象是模块的局部状态
    state.count++
    }
    },
    actions: {
    incrementIfOddOnRootSum (context) {
    if ((context.state.count + context.rootState.count) % 2 === 1) {
    context.commit('increment')
    }
    }
    },
    getters: {
    doubleCount (state, getters, rootState) {
    console.log(rootState.count) // 获取的是根状态的count
    return state.count * 2
    }
    }
    }

    注意actions的context

    1
    2
    3
    4
    5
    6
    7
    actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
    if ((context.state.count + context.rootState.count) % 2 === 1) {
    context.commit('increment')
    }
    }
    }

    { state, commit, rootState }对应context对象中的属性,使用ES6的对象解构。

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的, 这样使得多个模块能够对同一 mutation 或 action 作出响应。

我们已经知道了,模块内部的 action、mutation 和 getter 默认是注册在全局命名空间的。如果我们只想让他们在当前的模块中生效,应该怎么办呢?

通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

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
34
35
36
37
38
39
40
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,

// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},

// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},

// 进一步嵌套命名空间
posts: {
namespaced: true,

state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})

启用了命名空间的 getter 和 action 会收到局部化的 getterdispatchcommit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

在带命名空间的模块内访问全局内容(Global Assets)

如果你希望使用全局 state 和 getter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

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
modules: {
foo: {
namespaced: true,

getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},

actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'

dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
在带命名空间的模块注册全局 action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,

actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}

项目结构

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 ## 产品模块