Vuex的基本使用(保姆级)

140 阅读19分钟

背景

由于上一次写的文章不够直观,因此决定重写一下Vuex的基本使用。

Vuex的前置知识

什么是Vuex?

Vuex是一个状态管理工具,采用集中式存储管理应用的所有组件的状态,并且以相应的规则保证状态可预测的方式发生改变。本质就是把组件的共享状态和触发修改状态的函数抽离出来,这样就能获得状态或者触发行为。

什么情形下适合使用Vuex?

如果不打算开发大型单页应用,使用Vuex就会变得很冗余,就会有“牛刀杀鸡”的感觉,因为它会附带很多概念。这种情况下使用store模式就足够了。

如果正在开发一个中大型的单页应用,Vuex是更好的选择。

安装

直接下载

链接地址:unpkg.com/vuex@3.6.2/…,这个链接会一直指向在npm发布的最新版本。同样我们也可以指向其他的版本,比如unpkg.com/vuex@2.0.0,在项目中,通过script标签指向引用的文件。

npm

npm install vuex --save

yarn

yarn add vuex

经历以上的安装过程之后,需要在全局引入vuex,然后像router一样,使用Vue注册。

import Vuex from "vuex"
Vue.use(Vuex)

每一个Vuex的核心就是store,即仓库,可以说成是一个容器,它包含着大部分的共享状态。

Vuex的特点

  • Vuex的状态是响应式的,当Vue组件从store里面读取状态时。如果store中的状态发生变化,那么相应的消费组件也会高效的更新状态。
  • 开发者不能直接更改store中的状态,改变store中状态的唯一途径就是显示地提交mutation,这就使开发者可以很方便地跟踪每一个状态的变化。

Vuex的初体验

入口文件代码演示

下面是一个在入口文件中,从引入到使用的代码演示:

// index.js
import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0
  },
  mutations:{
    add(state){
      state.count++
    }
  }
})
// 4.触发mutation
store.commit("add")
console.log(store.state.count); //1
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

总结一下上面的步骤:

  1. 引入vuex;
  2. 全局注册Vuex,Vue.use(Vuex)
  3. 创建一个Vuex仓库,仓库里面可以包含五个选项:state(状态)、getters(对状态的过滤)、mutations(状态修改)、actions(异步提交修改状态信息)、modules(模块);
  4. 触发mutation:通过commit触发仓库中的状态更新,store.commit("对应的mutation")
  5. 将仓库注入到Vue对象中,也可以挂载到Vue对象原型上;

组件中获取状态、修改状态的代码演示

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="add">+1</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
export default {
    components:{Child2},
    // 1.获取state
    computed:{
        count(){
            return this.$store.state.count
        }
    },
    methods:{
        // 2.触发commit
        add(){
            this.$store.commit("add")
        }
    }
}
</script>

效果图: 小结:

  • 在组件中,使用this.$store获取仓库对象;
  • 获取通过computed方法获取,因为是需要不断地计算;

2.png

Vuex的核心概念

state

mapState辅助函数

这种模式是由于Vuex通过store选项,将Vuex注入到每一个组件中。但是,一个组件每次使用一个mutation,就要生成一个计算属性,这样就会使代码变得冗余。为了解决这个问题,我们可以使用mapState辅助函数帮助我们生成计算属性。

<script>
    // 第一步,引入mapState模块
    import {mapState} from "vuex"
    export default{
        // 第二步,获取状态
        // 方法一
        computed:mapState({
          count:state=>state.count
        }),
        // 方法二
        computed:mapState([
            "count"
        ]),
    }
</script>

小结:在组件中获取状态的三种方式

方式一,使用笨方法this.$store.state.相关的状态

<script>
    export default{
        computed:{
            count(){
                return this.$store.state.count
            }
        }
    }
</script>

方式二,引入mapState模块,接收对象

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:mapState({
        // 计算属性名:参数代表仓库的state
        count:state=>state.count
    })
}
</script>

效果图:

3.png 方式二小结:

  1. 需要从vuex模块引入mapState;
  2. mapState的键名代表计算属性的名称;
  3. mapState的键值是一个回调函数,回调函数的形参代表仓库的state;

方式三,引入mapState模块,接收数组参数

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:mapState([
        // 映射 this.count 为 store.state.count
        'count'
    ])
}
</script>

效果图:

3.png 小结方式三:

  1. 需要从vuex模块引入mapState;
  2. 如果需要引入数组的形式,比如['xxx'],就相当于将computed属性xxx,映射为仓库的xxx状态,即this.xxx => store.state.xxx,语法上不对,只是为了表达意思。
  3. 这种写法,引号里面是需要的state,然后使用state名称作为获取状态的变量;

对象展开操作符

上面总结了三种在computed属性里面获取状态的方式,后面几乎不会使用第一种方式,因为这种方式太过麻烦,而且“又老又土”。那么,问题来了,在computed属性里面写的都是mapState对象,怎么样才能将mapState对象和组件本身的计算属性混合在一起呢?

解决问题的办法就是,使用对象的展开操作符。

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{count}}
      <br/>
      {{localComputed}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:{
        localComputed(){
            return "我是局部的计算属性"
        },
        ...mapState([
            'count'
        ])
    }
}
</script>

在computed属性里面写...mapState({})或者...mapState([]),然后在里面正常获取store中的状态。

Getters

getters选项是store的计算属性,getters选项里面的函数,接受一个参数,这个参数就代表仓库的状态state。那么如何访问到经过处理后的getters属性值呢?

代码演示,需求:状态中的数组arr,只要done为true的项目

在main.js中设置getters:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  getters:{
    filterArr(state){
      return state.arr.filter(item=>item.done)
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中获取getters选项:

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{count}}
      <br/>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
export default {
    components:{Child2},
    computed:{
        count(){
            return this.$store.getters.filterArr
        }
    }
}
</script>

效果图:

4.png 上面提到过,getters选项是计算属性,它的返回结果是否有缓存,要分情况讨论。它如果通过state直接操作,并且返回处理后的state,这时候它没有缓存。如果基于其他的getter函数,返回state,这时候它有缓存。总而言之,getter函数依赖于state,就没有缓存;getter函数依赖于其他getter函数,就有缓存。

有缓存的情况

原理:一个getter函数接受两个参数,第一个参数代表仓库的所有状态,即state。第二个形参代表仓库中其他的所有getter函数。

代码演示,需求:数组过滤之后,返回一个新数组的长度。

在main.js中设置getters:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  getters:{
    // 获取done为true的数组
    filterArr(state){
      return state.arr.filter(item=>item.done)
    },
    // 获取新数组的长度
    getNewLength(state,getters){
      return getters.filterArr.length
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中获取getters选项:

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{count}}
      <br/>
      {{getNewLength}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
export default {
    components:{Child2},
    computed:{
        count(){
            return this.$store.getters.filterArr
        },
        getNewLength(){
            return this.$store.getters.getNewLength
        }
    }
}
</script>

效果图:

5.png

没有缓存的情况

需求:和上面的demo一样,数组过滤之后,返回一个新数组的长度。

在main.js中设置getters:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  getters:{
    // 获取新数组的长度
    getNewLength(state){
      return state.arr.filter(item=>item.done).length
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中获取getters选项:

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{getNewLength}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
export default {
    components:{Child2},
    computed:{
        getNewLength(){
            return this.$store.getters.getNewLength
        }
    }
}
</script>

效果图:

6.png

mapGetters辅助函数

如果在组件中获取getter属性值,使用mapGetter辅助函数更方便,和mapState函数类似。 具体代码演示放到后面。

小结:在组件中获取getter的三种方式

首先,main.js中的代码是这样的,主要关注store相关的代码:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  getters:{
    // 获取新数组的长度
    getNewLength(state){
      return state.arr.filter(item=>item.done).length
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

方式一:this.$store.getters.自定义属性

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{getNewLength}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
export default {
    components:{Child2},
    computed:{
        getNewLength(){
            // 获取getter属性
            return this.$store.getters.getNewLength
        }
    }
}
</script>

6.png

方式二:引入mapGetters模块,接收数组参数

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{getNewLength}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapGetters} from "vuex"
export default {
    components:{Child2},
    computed:{
        // 获取getter属性
        ...mapGetters([
            "getNewLength"
        ])
    }
}
</script>

6.png 和之前的mapState一样,计算属性名和getter属性名一样。

方式三:引入mapGetters模块,接收对象参数

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{getLength666}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapGetters} from "vuex"
export default {
    components:{Child2},
    computed:{
        // 获取getter属性
        ...mapGetters({
            "getLength666":"getNewLength"
        })
    }
}
</script>

6.png 一般情况下,个人觉得没必要用对象参数的形式,除非看getter属性名不爽,想自己搞一个。

和mapState不同,mapState的键值是一个回调函数,参数代表state。而mapGetter的键值是getter属性名,用引号括起来。

Mutation

mutations是vuex中的修改状态的方法,而且是唯一直接修改状态的方式,每一个mutation的属性值是一个回调函数,它的第一个参数代表,仓库的状态(state)。触发mutation的方法是通过调用store.commit("mutation属性名")

Mutation可以在控制台产生快照。

mutation的代码演示

在main.js中设置mutations:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  mutations:{
    // 声明一个mutation
    add(state){
      state.count++
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中提交mutation:

<template>
  <div class="child">
      我是Child组件
      <button @click="add">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    methods:{
        add(){
            this.$store.commit("add")
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

效果图:

7.png

传参

在vuex中,函数传参也被称作提交载荷(Payload)。mutation函数可以接收很多个参数,第一个参数代表仓库的所有状态,即state。从第二个参数及后面的参数,都是我们手动加上去的形参,接收我们提交时传的数据,这些数据也被官方地称作载荷。

代码演示

需求:按钮被点击后,加一个数,这个数是自定义的。

在main.js中设置mutations:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  mutations:{
    // 声明一个mutation
    add(state,n){
      state.count+=n
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中提交mutation:

<template>
  <div class="child">
      我是Child组件
      <button @click="add">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    methods:{
        add(){
            // 加10
            this.$store.commit("add",10)
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

效果图:

8.png 从代码运行原理上来看,mutation函数可以接收很多个参数,但是这样做,会导致代码整洁性不高,出错率增加。因此,实际开发中,只接受两个参数,第二个参数是一个对象,把想要传的参数都放在这个对象里面。

对象风格的传参方式

语法结构:

// 注册Mutation
mutations:{
  事件名(state,obj){
    // do something
  }
}
// 传参
store.commit({
  type:"事件名",
  objProperty:"xxx"
})

解释:注册时,和之前的方式一样。传参时,有差别。commit仅仅接收一个对象,type属性值是需要触发的事件名,后面的都是obj下面的键值对,相当于{objProperty:"xxx"} => obj

代码演示

需求:将上一页的需求再次通过对象传参的风格做一遍

在main.js中设置mutations:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0,
    arr:[
      {name:"鲁班",done:true},
      {name:"百里守约",done:false},
      {name:"娜可露露",done:true}
    ]
  },
  mutations:{
    // 声明一个mutation
    add(state,obj){
      state.count+=obj.n
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

在组件中提交mutation:

<template>
  <div class="child">
      我是Child组件
      <button @click="add">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    methods:{
        add(){
            // 加10
            this.$store.commit({
                type:"add",
                n:10
            })
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

实现了同样的效果

Vue的响应式规则

  1. 开发者最好在仓库的state中,初始化好所有需要的状态;
  2. 如果想在state中响应式添加一个对象,有两种情况:
  • 追加一个新对象:Vue.set(obj,"property",123)
  • 以新对象替换旧对象:state.obj={...state.obj,newProperty:123}
  1. Mutation函数必须是同步的。每一条mutation都会被devtools记录,devtools需要捕捉前一状态、后一状态。如果mutation函数中如果有异步函数,devtools无法得知异步函数什么时候执行,也就无法记录状态的变更。

小结:在组件中提交Mutation的三种方式

和获取state、getter一样,提交Mutation也有辅助函数mapMutations。由于mapMutations的本质是触发事件,因此放在methods中。

首先,main.js中的代码是这样的,主要关注Mutation相关的代码:

import Vue from 'vue'
import App from './App.vue'
// 1.引用
import Vuex from "vuex"

Vue.config.productionTip = false
// 2.全局注册
Vue.use(Vuex)
// 3.创建一个仓库
const store=new Vuex.Store({
  state:{
    count:0
  },
  mutations:{
    // 声明一个mutation
    add(state,obj){
      state.count+=obj.n
    }
  }
})
new Vue({
  // 5.注入到Vue对象
  store,
  render: h => h(App),
}).$mount('#app')

方式一:笨方法this.$store.commit({type:"事件名",……})

<template>
  <div class="child">
      我是Child组件
      <button @click="add">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    methods:{
        add(){
            // 提交Mutation
            this.$store.commit({
                type:"add",
                n:10
            })
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

方式二,引入mapMutations模块,接收对象属性参数

<template>
  <div class="child">
      我是Child组件
      <button @click="clickFn">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapMutations} from "vuex"
export default {
    components:{Child2},
    methods:{
        ...mapMutations({
            // 将this.add666 映射为 this.$store.commit("add")
            add666:"add"
        }),
        clickFn(){
            this.add666({n:10})
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

说明:

  1. mutation需要事件触发,比如点击事件;
  2. mapMutation的对象,键名是自定义的触发事件名键值是之前在mutations选项中触发的事件名
  3. 在触发事件中,使用this.自定义触发事件名(实参),由于在mapMutation对象中,已经完成了映射关系,这里的this.自定义触发事件名(实参)===this.$store.commit("触发事件名",实参)

方式三,引入mapMutations模块,接收数组属性参数

<template>
  <div class="child">
      我是Child组件
      <button @click="clickFn">+1</button>
      <br/>
      {{count}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapMutations} from "vuex"
export default {
    components:{Child2},
    methods:{
        ...mapMutations([
            // 将this.add 映射为 this.$store.commit("add")
            "add"
        ]),
        clickFn(){
            this.add({n:10})
        }
    },
    computed:{
        ...mapState(["count"])
    }
}
</script>

数组就是不需要自定义触发事件名自定义触发事件名之前的触发事件名是同一个名字,也就是:

..mapMutations({
    // 将this.add 映射为 this.$store.commit("add")
    add:"add"
}),

简写为:

..mapMutations([
    // 将this.add 映射为 this.$store.commit("add")
    add:"add"
]),

Actions

actions选项的功能和mutations选项类似,但是也有不同之处:

  1. Actions提交的是mutation,而不是直接变更状态;
  2. Actions里面可以包含任意的异步操作;

所有的Action函数都接受一个与store实例具有相同方法和属性的context对象,这里的context对象指向当前的仓库,也就是store。因此在函数体中可以调用context.commit提交触发状态,相当于store.commit。这里注意,context对象不是store对象本身,这里相当于复制体。

action初体验

代码演示:

const store=new Vuex.Store({
  state:{
    count:0
  },
  mutations:{
    // 声明一个mutation
    add(state,obj){
      state.count+=obj.n
    }
  },
  actions:{
    // 声明一个action
    addOne(context){
      context.commit("add",{n:10})
    }
  }
})

这样我们就声明好了一个action,再次强调:只接收一个参数,代表当前的仓库对象store

由于是对象,我们可以采取ES6对象解构赋值的方式完成上面的操作。

const store=new Vuex.Store({
  state:{
    count:0
  },
  mutations:{
    // 声明一个mutation
    add(state,obj){
      state.count+=obj.n
    }
  },
  actions:{
    // 声明一个action
    addOne({commit}){
      commit("add",{n:10})
    }
  }
})

这样就省略一些代码,同时也消除掉了无用的代码。(这里无用的代码指的是context,既然不对context对象进行任何操作,干嘛特意去声明它?)

action函数的触发

语法:store.dispatch(),注意,具体的触发方式和Mutation函数一样,分为普通触发(载荷触发)、对象方式触发。

这里面先把main.js的代码写好:

const store=new Vuex.Store({
  state:{
    count:0
  },
  mutations:{
    // 声明一个mutation
    add(state,obj){
      state.count+=obj.n
    }
  },
  actions:{
    addOne(context){
      context.commit("add",{n:10})
    }
  }
})

普通方式触发 语法:store.dispatch('action自定义事件名',实参),和mutation触发类似,第一个参数是自定义事件名,第二个参数是一个实参,通常是对象。

代码演示:

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="clickFn">+10</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(["count"])
    },
    methods:{
        clickFn(){
            // 触发action 
            this.$store.dispatch('addOne')
        }
    }
}
</script>

对象方式触发

语法:store.dispatch({type:"action自定义事件名",……键值对形式的实参……})

代码演示:

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="clickFn">+10</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(["count"])
    },
    methods:{
        clickFn(){
          // 对象方式触发action
            this.$store.dispatch({
                type:"addOne"
            })
        }
    }
}
</script>

小结:在组件中触发(分发)action的三种方式

在组件中分发,有辅助函数mapActions。

方式一:使用笨方法this.$store.dispatch('action自定义事件名',实参)

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="clickFn">+10</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(["count"])
    },
    methods:{
        clickFn(){
            // 触发action 
            this.$store.dispatch('addOne')
        }
    }
}

方式二,使用mapActions模块,接收参数的类型是对象

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="clickFn">+10</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapActions} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(["count"])
    },
    methods:{
        // 触发action
        ...mapActions({
            addSomething:"addOne"
        }),
        clickFn(){
            this.addSomething()
        }
    }
}
</script>

方式三,使用mapActions模块,接收参数的类型是数组

<template>
  <div class="child">
      我是Child组件
      {{count}}
      <button @click="clickFn">+10</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapActions} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(["count"])
    },
    methods:{
        // 触发action
        ...mapActions(["addOne"]),
        clickFn(){
            this.addOne()
        }
    }
}
</script>

对于以上三种触发方式,不再赘述,如果不动请参照Mutations选项。

了解action的返回结果

store.dispatch的返回结果就是Promise对象,因此也返回Promise对象。既然是Promise对象,我们可以在它触发action成功之后,做一些事情。

上代码:

// main.js
const store=new Vuex.Store({
 actions:{
     actionA({commit},amount){
         return new Promise((resolve,reject)=>{
             setTimeout(()=>{
                 commit('increment')
                 resolve()
             },1000)
         })
     }
 }
})
<script>
// 组件
 export default{
     store.dispatch('actionA').then(()=>{
         // 成功之后的回调
     })
 }
</script>

而且,我们也可以这么玩:在另一个action函数中,触发actionA:

const store=new Vuex.Store({
 actions:{
     actionA({commit},amount){
         return new Promise((resolve,reject)=>{
             setTimeout(()=>{
                 commit('increment')
                 resolve()
             },1000)
         })
     },
     // 下面是actionB
     actionB({commit,dispatch}){
         return dispatch('actionA').then(()=>{
             commit('otherMutation')
         })
     }
 }
})

经过上面这么一折腾,action函数就显得非常灵活。小结一下,第一,我们可以Promise对象的特性,及时拿到成功、或者失败的反馈。第二,我们也可以在任何一个action函数中,处理其他action函数。

组合触发多个action

需求是这样的,在actionB函数中触发actionA函数,等触发完之后,再去做自己的事情。

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

Modules

根据上面学习到的内容,一个store对象中包含状态(state)、状态处理(getters)、同步处理事件(mutations)、异步处理事件(actions)。我们就会看到store对象变得非常臃肿。为了解决这个问题,Vuex允许我们将store对象分割成多个模块,modules。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

modules初体验

需求:获取不同模块的状态,并且显示到页面

// main.js
// 3.创建一个仓库
const moduleA={
  state:{
    countA:"aaaaa"
  }
}
const moduleB={
  state:{
    countB:"bbbbb"
  }
}
const store=new Vuex.Store({
  modules:{
    aaa:moduleA,
    bbb:moduleB
  }
})

组件(.vue文件):

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{aaa.countA}}
      <br/>
      {{bbb.countB}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(['aaa','bbb'])
    }
}
</script>

效果图:

9.png 小结:

  1. store.state.aaa表示moduleA的全部状态;
  2. 这样分开写,代码逻辑更加清晰;

模块的局部状态

对于模块内部的getter函数和mutation函数,接收的第一个参数state,代表当前模块的局部状态对象。

Mutation代码演示

main.js

// 3.创建一个仓库
const moduleA={
  state:()=>({
    countA:"aaaaa"
  }),
  mutations:{
    addOneA(state){
      state.countA=state.countA.length
    }
  }
}
const moduleB={
  state:{
    countB:"bbbbb"
  },
  mutations:{
    addB(state){
      state.countB=state.countB+"       我是B组件的附加内容"
    }
  }
}
const store=new Vuex.Store({
  modules:{
    aaa:moduleA,
    bbb:moduleB
  }
})

组件文件:

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{aaa.countA}}
      <br/>
      {{bbb.countB}}
      <br/>
      <button @click="clickFn">转变模块A的数据</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapMutations} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(['aaa','bbb'])
    },
    methods:{
        ...mapMutations(['addOneA','addB']),
        clickFn(){
            this.addOneA()
            this.addB()
        }
    }
}
</script>

按钮点击前的效果图:

10.png

按钮点击后的效果图:

11.png

getter函数接收三个参数,分别是state、getters、rootState,第二个参数是当前模块的其他getter函数,第三个参数是根节点状态。

Getter函数代码演示

main.js:

// 3.创建一个仓库
const moduleA={
  state:()=>({
    countA:"aaaaa",
    arrA:[1,9,6,45,4]
  }),
  getters:{
    // 过滤出大于1的数据
    filterArrA(state,getters,rootState){
      return rootState
    }
  }
}
const moduleB={
  state:{
    countB:"bbbbb"
  }
}
const store=new Vuex.Store({
  modules:{
    aaa:moduleA,
    bbb:moduleB
  }
})

组件文件(.vue):

<template>
  <div class="child">
      我是Child组件
      <br>
        {{filterArrA}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapGetters} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapGetters(['filterArrA'])
    }
}
</script>

效果图:

12.png 根节点状态就是,包含所有模块状态信息的对象。

对于action函数来说,接收一个对象参数,对象下面挂载state、commit、rootState等参数,分别代表局部状态、提交mutation、根节点状态。

action函数代码演示

const moduleA = {
  // ...
  getters: {
    // 局部状态state、其他的getter、根节点状态
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

命名空间

回顾一下mutation函数的代码展示:

main.js

// 3.创建一个仓库
const moduleA={
  state:()=>({
    countA:"aaaaa"
  }),
  mutations:{
    addOneA(state){
      state.countA=state.countA.length
    }
  }
}
const moduleB={
  state:{
    countB:"bbbbb"
  },
  mutations:{
    addB(state){
      state.countB=state.countB+"       我是B组件的附加内容"
    }
  }
}
const store=new Vuex.Store({
  modules:{
    aaa:moduleA,
    bbb:moduleB
  }
})

组件文件:

<template>
  <div class="child">
      我是Child组件
      <br/>
      {{aaa.countA}}
      <br/>
      {{bbb.countB}}
      <br/>
      <button @click="clickFn">转变模块A的数据</button>
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapState,mapMutations} from "vuex"
export default {
    components:{Child2},
    computed:{
        ...mapState(['aaa','bbb'])
    },
    methods:{
        ...mapMutations(['addOneA','addB']),
        clickFn(){
            this.addOneA()
            this.addB()
        }
    }
}
</script>

仔细观察组件文件中,触发mutation时,可以通过this.$store触发。这是问什么?

因为在默认情况下,模块内部的action、mutation、getter是注册到全局命名空间的。

如果想要代码有更高的封装性和复用性,还需要该模块称为具名的空间模块,实现方式是,在modules选项里面添加namespaced:true。当模块被注册后,它的所有相关的getter、mutation、action函数会根据注册时的路径,调整命名。

代码演示:

// 3.创建一个仓库
const moduleA={
  namespaced:true,
  state:()=>({
    countA:"aaaaa",
    arrA:[1,9,6,45,4]
  }),
  getters:{
    // 获取根节点所有状态
    filterArrA(state,getters,rootState){
      return rootState
    }
  }
}
const store=new Vuex.Store({
  modules:{
    a:moduleA
  }
})

.vue:

<template>
  <div class="child">
      我是Child组件
      <br>
        {{filterArrA}}
      <Child2/>
  </div>
</template>

<script>
import Child2 from '@/components/Child2'
import {mapGetters} from "vuex"
export default {
    components:{Child2},
    computed:{
      // 注意获取方式
        ...mapGetters({filterArrA:['a/filterArrA']})
        // filterArrA(){
        //     return this.$store.getters['a/filterArrA']
        // }
    }
}
</script>

效果图:

13.png 如果vuex是以模块化开发的,在组件文件中,获取数据的方法如下:

  • state:this.$store.state.模块名.key
  • getters:this.$store.getters['模块名/key']
  • mutations:this.$store.commit('模块名/method')
  • actions:this.$store.dispatch('模块名/method')

模块里面也可以进一步嵌套模块,原理是一样的。如果有namespaced:true属性,就通过父级模块名/子级模块名/getter方法名进行取值。如果有该属性,就通过父级模块名/getter方法名

代码演示:

main.js

const store=new Vuex.Store({
    modules:{
        account:{
            namespaced:true,
            // 下面是模块内容,module assets
            state:()=>({}),
            getters:{
                isAdmin(){}  // getters的模块是getters['account/isAdmin']
            },
            actions:{
                login(){}  // dispatch('account/login')
            },
            mutations:{
                login(){}  // commit('account/login')
            }
            // 下面是嵌套模块,由于没有namespaced属性,所以继承父模块的命名空间
            modules:{
                myPage:{
                    state:()=>({}),
                    getters:{
                        profile(){}  // getters['account/profile']
                    }
                },
                // 进一步嵌套
                posts:{
                    namespaced:true,
                    state:()=>({}),  
                    getters:{
                        popular(){}  // getters['account/posts/popular'] 
                    }
                }
            }
        }
    }
})

借用了官网的demo,实现思路和前一个demo如出一辙,这里不再赘述。

如果启动了命名空间,模块的state、getter、mutation、action都是局部的。否则,就继承父级模块的命名空间。

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

如果你希望在带有namespaced属性的模块内使用全局的state、getter。在getter函数内,接收rootState、rootGetters作为第三、第四个参数,从而获得全局的state。在action函数内,也可以通过context对象的属性传入。

如果需要在命名模块内,分发action(dispatch)或者提交mutation(commit),将{root:true}做为第三个参数传给dispatch或者commit即可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`方法
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter',调用foo模块下的其他getter方法
        rootGetters.someOtherGetter // -> 'someOtherGetter',调用根节点下的getter方法
      },
      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) { ... }
    }
  }
}

dispatch和commit的异同点?

相同点:

  • 作用:commit 和 commit 两种方法都是提交mutation,进而改变状态;

  • 语法

// 第一个参数:触发的mutation
// 第二个参数:载荷,可以是任何类型,推荐对象的形式
// 第三个参数:选项,类型对象形式
store.commit(type,payLoad,options)

不同点:

  1. 存取方式不同,commit用来提取当前模块的mutations,dispatch用来提交当前模块的actions(actions再去提交mutations),可以进行异步操作。因此,有些情况下,commit 做不到的,dispatch进行提交。
  2. commit的原理是直接通过mutations修改state;dispatch的原理则是通过action修改mutations,再通过mutations修改state。看起来效果都是修改了state,但是如果想在修改完做点其他事情,dispatch比较方便。
  3. 作用上更官方的解释是,commit提交mutation,dispatch分发action。提交mutation时,options 里可以有 root: true,它允许在命名空间模块里提交根的 mutation;分发action时,options 里可以有 root: true,它允许在命名空间模块里分发根的 action。返回一个解析所有被触发的 action 处理器的 Promise。
  4. 简单来说,使用commit触发mutation,使用dispatch触发action。

小结

  1. 在具名模块中,getter函数接收四个参数,分别是state、getters、rootState、rootGetter,前两个分别代表着当前模块的state和getter,后两个分别代表着根节点的所有state和getter方法。
  2. 在具名模块中,action函数接受的context对象参数,下面有一个rootGetters属性,里面保存着根节点的所有状态。
  3. 分发action或者提交mutation时,需要添加{root:true}选项,作为第三个参数。