你真的懂Vuex吗?(附带demo)

868 阅读6分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
不官方解释:说白了就是提供一个统一管理数据的解决方案,并且能够根据数据的改变去重新渲染页面。

文章demo下载

本教程使用的demo已经上传git仓库,需要的同学可以自行下载,传送门🚂点击传送🏍

本教程使用的demo已经上传git仓库,需要的同学可以自行下载,传送门🚂点击传送🏍

本教程使用的demo已经上传git仓库,需要的同学可以自行下载,传送门🚂点击传送🏍

为啥要使用Vuex

下面我们在看两张图来理解为啥要使用Vuex

组件系统是Vue的一个重要概念,一个标准的Vue页面下会存在相互嵌套的子组件,那么在页面传递数据时就需要一层一层向下传递,若是头部组件的数据需要传递到左侧内容组件中,将会变得更加复杂。

image.png

那么Vuex的出现就解决了这种复杂的参数传递方式。我们可以把Vuex看做一个独立于所有组件之外的全局单例模式,只要是Vuex中的数据源,所有组件都可以使用。那么就不会存在上述问题了。

image.png

1、创建项目并引入Vuex

1.1安装Vuex

在终端中输入以下命令

npm install vuex --save

1.2创建相应的文件

在src文件夹中创建store文件夹,并新建index.js文件,输入以下代码创建一个 store

image.png

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: { 
  },
  actions: {
  },
  modules: {
  }
})

1.3注入Vuex

为 Vue 实例提供创建好的 store,通过“注入”的方式在mian.js文件中编写以下代码

import Vue from 'vue'
import App from './App.vue'
import store from './store'  //关键代码1

Vue.config.productionTip = false

new Vue({
  store,   //关键代码2
  render: h => h(App)
}).$mount('#app')

至此你已经完成了最基本的操作,接下来就可以学习并使用Vuex了。

2、Vuex核心概念

名称解释
State单一状态树,其实说白了就是用来存储数据的数据源,可以看做 vue 实例中的 data
Getters可以把它看做一个 vue 实例中的计算属性,它可以返回处理过的 State 数据,返回值也会根据它的依赖值被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Mutations用于定义改变 State 数据的函数且必须为同步函数,要更改 Vuex 的 store 中的 状态(State) 的唯一方法是提交 mutation
Actions用于执行异步操作,因为 Mutation 必须是同步函数,所以需要异步变更数据时,就需要用 Action 异步提交 mutation ,mutation再执行数据变更
Modules用于将 store 分割成 模块(module),每个模块都会包含state、mutation、action、getter、甚至是嵌套子模块并且以从上至下进行同样方式的分割

先看看整体的基本代码结构

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

2.1 State

我们通过几个案例来了解 state 是如何使用的。先定义几个默认数据

export default new Vuex.Store({
  state: {
    title1:"这是标题1",
    title2:"这是标题2",
    title3:"这是标题3",
  },
})

基本使用

方式一:直接在双括号中使用
方式二:在生命周期钩子函数中给data赋值
方式三:在计算属性中引入

<template>
  <div id="app">
    <!-- 方式一:直接在双括号中使用 -->
    <h1>{{$store.state.title1}}</h1>
    <h2>{{title2}}</h2>
    <h3>{{title3}}</h3>
  </div>
</template>

<script>
export default {
  name: 'App',
  data(){
    return{
      title2: ""
    }
  },
  created(){
    // 方式二:在生命周期钩子函数中给data赋值
    this.title2 = this.$store.state.title2
  },
  computed: {
    // 方式三:在计算属性中引入
    title3 () {
      return this.$store.state.title3
    }
  }
}
</script>

扩展使用(mapState)

当我们多个数据时,正常使用this.$store.state.***写法的话就会出现如下代码,这样看起来非常的不美观且冗余。

computed: {
    title () {
      return this.$store.state.title1 + this.$store.state.title2 + this.$store.state.title3
    }
  }

为了解决上面那个问题,我们可以使用 mapState 辅助函数,因为mapState返回的是一个对象,所以我们可以使用展开运算符,将它与局部计算属性混合使用。

用法一:

  data(){
    return{
      mark:"mapState"
    }
  },
  computed: {
    // 原计算属性
    title () {
      return "原计算属性"
    },
    // 使用展开运算符
    ...mapState({

      //写法一 只有这样才能使用 this 获取相关数据 
      title1(state){
        return this.mark + state.title1;
      },

      //写法二  使用箭头函数使代码变得简练
      title2: state => state.title2,

      //写法三  'title3' 等同于 state => state.title3
      title3: 'title3', 
    }),
  }

用法二:

mapState还支持使用字符串数组进行映射返回,用法如下:

computed: {
    //使用数组返回将更加简单,不过无法进行数据操作
    ...mapState(['title1','title2','title3'])
}

添加state新属性

Vuex的store中存储的状态(数据)是响应式的,那么在我们变更状态(数据)时,组件绑定中的状态(数据)也会随之改变,所以在添加state新属性时应该使用Vue.set(obj,'newName','新添加数据')或者对象扩展运算符进行添加或替换老对象。

//   通过set设置新参数
this.$set(this.$store.state,"obj",{name:'姓名',name2:'姓名2'})
console.log("通过set设置新参数:",this.$store.state.obj)

//   使用展开运算符设置新参数
this.$store.state.obj = {
  ...this.$store.state.obj,
  name2:"修改后的姓名2",
  name3:"姓名3"
}
console.log("使用展开运算符设置新参数:",this.$store.state.obj)

效果:

image.png

2.2 Getters

我们可以认为 Gettersstore 的计算属性,它的特性也和计算属性一样,getter 的返回值会根据依赖值缓存起来,当依赖值发生改变时会重新计算并返回,接下来我们看看它的基本使用

基本使用

Getter 中一共有 stategettersrootStaterootGetters 四个参数,参数说明如下

参数说明数据类型
state参数来访问vuex中存储的状态(数据源)Object
getters参数来访问vuex中其他 getterany
rootState参数用来访问根节点存储的状态(数据源)Object
rootGetters参数用来访问根节点的其他 getterany
Getter(state: {}, getters: any, rootState: {}, rootGetters: any)

这里不对 rootStaterootGetters 这两个参数进行讲解,等学习到 Modules 时再进行讲解

接下来通过案例来理解stategetters参数的使用

定义以下 getters

🥇在getTitle中使用了state参数进行title1和title2的拼接

🥈在getTitleAll中使用了gettersstate参数进行拼接 getTitle 和 title3

export default new Vuex.Store({
  state: {
    title1:"这是标题1",
    title2:"这是标题2",
    title3:"这是标题3",
  },
  getters: {
    // 使用 state 参数,拼接 title1 和 title2 属性
    getTitle(state){
      return  state.title1 + state.title2;
    },
    // 使用 getters参数, 拼接 getTitle 和 title3
    getTitleAll(state,getters){
      return getters.getTitle + state.title3;
    }
  }
})

在vue组件中使用,如下代码。

<template>
  <div>
    <!-- 方式一:直接使用 -->
    <h1>{{ $store.getters.getTitle }}</h1>
    <h2>{{ titleAll }}</h2>
  </div>
</template>

<script>
export default {
  computed: {
    titleAll(){
      // 方式二:通过 this.$store.getters.** 进行获取
      return this.$store.getters.getTitleAll;
    }
  }
}
</script>

通过上面的介绍,我相信你们都发现了一个问题,就是getters中没有用来接收传参的参数,那么我们该如何实现getter的传参呢,接下来我们看这个例子

通过getter返回一个函数,这个函数用来接收getter的传参,下面两种写法的效果都是一样的,写法如下

🥈第一种方法,为了照顾那些不太熟悉箭头函数的同学

🥇第二种方法,使用箭头函数,让代码看起来更加简练

export default new Vuex.Store({
  state: {
    title1:"这是标题1",
  },
  getters: {
    //返回一个函数,并接收一个传递参数
    splicingTitle(state){
      return function(value){
        return state.title1 + value;
      }
    },
    //使用箭头函数和接受传递的对象参数
    splicingTitle2: (state) => (obj) => state.title1 + obj.value,
  }
})

用法:

<template>
  <div>
    <h3>{{ splicingTitle }}</h3>
    <h3>{{ splicingTitle2 }}</h3>
  </div>
</template>

<script>
export default {
  computed: {
    splicingTitle(){
      // 传递字符串参数
      return this.$store.getters.splicingTitle("这是传递的参数");
    },
    splicingTitle2(){
      // 传递对象参数
      return this.$store.getters.splicingTitle2({value:"对象传参"});
    }
  }
}
</script>

扩展使用(mapGetters)

mapGetters辅助函数主要是将store中的getter映射到局部计算属性

需要注意的是,如果返回为字符串则直接以数据形式使用,若是返回的是函数则以函数的形式调用
代码如下:

<template>
  <div>
    <!-- 返回为字符串则以数据形式使用 -->
    <h1>{{ getTitle }}</h1>
    <h1>{{ allTitle }}</h1>
    <!--返回为函数则以函数形式调用 -->
    <h1>{{ splicingTitle("参数A") }}</h1>
    <h1>{{ splicingTitle2({value}) }}</h1>
  </div>
</template>

<script>
//先引入 mapGetters
import { mapGetters } from 'vuex'

export default {
  data(){
    return{
      value:"参数B"
    }
  },
  computed: {
    //使用对象展开运算符将 getter 混入 computed 对象中
    // 数组映射形式
    ...mapGetters(['getTitle','splicingTitle']),

    // 对象映射
    ...mapGetters({
      // 另取别名映射
      allTitle:'getTitleAll',
      splicingTitle2:'splicingTitle2',
    })
  }
}
</script>

2.3 Mutations

在 Vuex 中更改 store 中的状态唯一的方法就是提交 mutation ,这里所说的提交mutation并不是调用函数,而是使用 store.commit 进行提交。

每个mutation都有一个字符串类型的 type 和一个回调函数 handler,回调函数会将 state 作为第一个参数,后续参数则为自定义的传入参数,但是大多数情况下传入参数一般会使用对象传递,后续会讲到。

基本使用

这里提前定义两个 mutation,供后面的讲解使用

export default new Vuex.Store({
  mutations: {
    // updateTitle 作为type  ,箭头函数作为 mutation 的回调函数
    "updateTitle":(state)=>{
      state.title1 = "mutation修改后的标题1"
    },
    // 简写形式,并传入额外参数
    updateTitle2(state,obj){
      state.title2 = "mutation修改后的标题2"
    }
  },
})

上面说到提交mutation并不是调用函数,而是使用store.commit进行提交,请看下面例子是如何使用的,这里一共列出了三种提交形式,至于使用哪一种都是没有问题的。

<template>
  <div>
      <h2>{{$store.state.title1}}</h2>
      <button @click="updateTitle">修改状态(不传递额外参数)</button>

      <h2>{{$store.state.title2}}</h2>
      <button @click="updateTitle2">修改状态(传递额外参数)</button>
      <button @click="updateTitle3">修改状态(以对象形式传递额外参数)</button>
  </div>
</template>

<script>
export default {
    methods:{
        updateTitle(){
            //直接提交不携带参数
            this.$store.commit("updateTitle")
        },
        updateTitle2(){
            // 提交并携带一个对象作为额外参数
            this.$store.commit("updateTitle2",{value:"额外参数"})
        },
        updateTitle3(){
            // 以对象的形式提交,并携带参数
            this.$store.commit({
                type:"updateTitle2",
                value:"以对象形式提交额外参数"
            })
        }
    }
}
</script>

扩展使用(mapMutations)

类似的辅助函数我们在上面已经讲过两次了,这里不做过多赘述,同样是先将引入 mapMutations 辅助函数,后续则是函数就调用函数,非函数则直接使用

<template>
  <div>
      <h2>{{$store.state.title1}}</h2>
      <button @click="updateTitle">修改状态(不传递额外参数)</button>

      <h2>{{$store.state.title2}}</h2>
      <button @click="updateTitle2({value:'mapMutations'})">修改状态(传递额外参数)</button>
  </div>
</template>

<script>
// 引入 mapMutations 辅助函数
import {mapMutations} from 'vuex'
export default {
    methods:{
        ...mapMutations(['updateTitle','updateTitle2']),
    }
}
</script>

mutation必须同步函数

最初我们有讲到mutation必须为同步函数,为什么一定是同步函数呢,下面我将举个例子进行解释。

我们先定义两个 mutation 一个为同步,一个为异步

mutations: {
    // 同步函数
    addcount(state){
      state.count++;
    },
    // 异步函数
    syncAddcount(state){
      setTimeout(() => {
        state.count++;
      }, 1000);
    }
},

让我们在页面中调用并通过 Vue Devtools 面板进行观察,如果没有安装 Vue Devtools 面板的同学,请自行百度安装

<template>
  <div>
      <h2>{{$store.state.count}}</h2>
      <button @click="addcount">提交同步方法</button>
      <button @click="syncAddcount">提交异步方法</button>
  </div>
</template>

<script>
export default {
    methods:{
        addcount(){
            this.$store.commit("addcount")
        },
        syncAddcount(){
            this.$store.commit("syncAddcount")
        }
    }
}
</script>

Vue Devtools 面板: image.png

当我们执行同步函数时,可以看到正常出现执行记录和 state 中 count 值也正常发生了改变

image.png

当我们再去执行异步函数时,我们可以看到执行记录是有了,但是 count 值在 state 并没有发生改变,但是页面上的 count 值却正常显示成了数字2

image.png

image.png

通过上面的案例我们发现当在mutation中混合异步调用会导致程序的调试变得十分困难,所以为了下一节说到了 Actions 就能很好的解决这种异步调用

2.4 Actions

Action 的主要作用是提交 mutation 而不是像 mutation 那样直接改变状态(数据),Action支持包含任意的异步操作。

Action 函数会接受 context 对象作为第一个参数,context对象与 store 实例拥有相同的方法和属性,你可以把它看做一个store。

基本使用

下面我们通过一个案例了解如何注册并使用一个简单的 action ,上面说到 mutation 必须是同步函数,而 Action 则不会受到这个约束,那我们也一起看看异步的 Aciton

actions: {
    // 当 addcountAction 被执行时,会提交一个 mutation
    addcountAction(context){
      context.commit("addcount");
    },
    // 异步Actions
    syncAddcountAction(context){
      setTimeout(() => {
        context.commit("addcount");
      }, 1000);
    }
  },

Action 是通过 store.dispatch 方法进行触发:

<template>
    <div>
        <h2>{{$store.state.count}}</h2>
        <button @click="addcountAction">执行同步 Action </button>
        <button @click="syncAddcountAction">执行异步 Action </button>
    </div>
</template>

<script>
export default {
    methods:{
        addcountAction(){
            this.$store.dispatch("addcountAction")
        },
        syncAddcountAction(){
            this.$store.dispatch("syncAddcountAction")
        }
    }
}
</script>

接下来我们通过 Vue Devtools 面板观察数据的变化

执行同步 Action 函数,函数中提交了一个类型为 addcount 的 mutation ,这里数据变化是没有问题的

image.png

接下来我们在执行异步 Action 函数,函数中同样提交了一个类型为 addcount 的 mutation ,这里数据也正常发生了改变,至此 mutation 无法执行异步函数的问题已经解决!!

image.png


上面说到 mutation 是可以携带参数的,那么 Action 呢?其实 Action 携带参数的用法和 mutation 是完全一样的。

定义action

//携带参数
updateTitle2Action(context,obj){
  context.commit({
    type:"updateTitle2",
    value:obj.value
  });
}

调用action

this.$store.dispatch({
     type:'updateTitle2Action',
     value:'action参数传递'
 })

扩展使用(mapActions)

又是一个辅助函数, Vuex 中的每一个核心概念都会有这样一个辅助函数,用法也是基本相同的,就直接贴代码了,有需要的直接去下载项目进行 mapActions 写法尝试

<template>
    <div>
        <h2>{{$store.state.count}}</h2>
        <button @click="addcountAction">执行同步 Action </button>
        <button @click="syncAddcountAction">执行异步 Action </button>

        <h2>{{$store.state.title2}}</h2>
        <button @click="btnClick">携带参数的 Action </button>
    </div>
</template>

<script>
//引入 mapActions
import {mapActions} from 'vuex'
export default {
    methods:{
        ...mapActions(['addcountAction','syncAddcountAction','updateTitle2Action']),
        // 需要携带参数可以这样写
        btnClick(){
             this.updateTitle2Action({
                 value:'action参数传递'
             })
        }
    }
}
</script>

组合 Action (异步扩展)

我们知道 Action 是可以异步执行的,那我们怎么知道这个异步任务什么时候结束呢,若我们需要组合使用多个 Action 又该怎么进行操作呢?

首先我们要知道 store.dispatch 本身是会返回 Promise 的,如果执行的 Action 不返回 Promise 则在执行成功之后直接返回 Promise ,如果执行的 Action 返回 Promise 则会处理返回的 Promise。我们通过下面这里例子看一下

我们定义三个 action 分别是

🥇 combinationA 异步执行 action 不返回 Promise

🥈 combinationB 异步执行 action 返回 Promise

🥉 combinationC 等待 combinationB 执行结束再执行

// 异步 action 不返回 Promise
combinationA(context){
  setTimeout(() => {
    context.commit("addcount");
    console.log("combinationA执行完成");
  }, 2000);
},
// 异步 action 返回 Promise
combinationB(context){
  return new Promise((resolve) =>{
    setTimeout(() => {
      context.commit("addcount");
      console.log("combinationB执行完成");
      resolve();
    }, 2000);
  })
},
// 组合 action ,  combinationB 执行完成之后再执行 combinationC
combinationC(context){
  return new Promise( resolve =>{
    context.dispatch("combinationB").then(()=>{
      context.commit("addcount");
      resolve();
      console.log("combinationC执行完成");
    })
  })
}

上面这三个 action 都有对应的控制台输出,我们再结合 store.dispatch 来看看最后的执行步骤

<template>
    <div>
        <h2>{{$store.state.count}}</h2>
        <button @click="btnClick1">combinationA</button>
        <button @click="btnClick2">combinationB</button>

        <h2>{{$store.state.title2}}</h2>
        <button @click="btnClick3">combinationC</button>
    </div>
</template>

<script>
import {mapActions} from 'vuex'
export default {
    methods:{
        ...mapActions(['combinationA','combinationB','combinationC']),
        btnClick1(){
             this.combinationA({
                 value:'action参数传递'
             }).then(res =>{
                console.log("dispatch 执行成功",res)
             }).catch(err => {
                console.log("dispatch 执行失败",err)
             })
        },
        btnClick2(){
             this.combinationB({
                 value:'action参数传递'
             }).then(res =>{
                console.log("dispatch 执行成功",res)
             }).catch(err => {
                console.log("dispatch 执行失败",err)
             })
        },
        btnClick3(){
             this.combinationC({
                 value:'action参数传递'
             }).then(res =>{
                console.log("dispatch 执行成功",res)
             }).catch(err => {
                console.log("dispatch 执行失败",err)
             })
        }
    }
}
</script>

回顾一下三个 Action 的定义,然后看一下控制台是否输出正确

🥇 combinationA 异步执行 action 不返回 Promise

image.png

🥈 combinationB 异步执行 action 返回 Promise image.png

🥉 combinationC 等待 combinationB 执行结束再执行

image.png

2.5 Module

来到最后一个核心概念 Module ,通过学习了上面的知识之后,我们不难发现如果当应用的状态(数据)非常庞大,又全部集中在一起时,就会变得十分难以管理,为了解决这种问题, Vuex 允许我们将 store 分割成模块(module),而每个模块都会拥有自己的 state、mutation、action、getter。下面我们通过案例来熟悉 Modules 这个核心概念。

基本使用

首先在项目中的 store 文件夹下 modules 文件夹中先定义两个模块

image.png

代码如下: 这里分别在模块中定义了

home/index.js

const homeModule={
    state:()=>({
        username:"小王",
        userId:"wdnmd_123456"
    }),
    getters:{
        usernameHandle(state){
            return "账号名称:"+state.username;
        }
    }
}
export default{
     //使用展开运算符导出 homeModule 各个核心概念
    ...homeModule
}

order/index.js

let orderModule={
    state:()=>({
        orderCount:"99"
    }),
    getters:{
        orderCountHandle(state){
            return "订单总数:"+state.orderCount;
        }
    }
}
export default{
    //使用展开运算符导出 orderModule 各个核心概念
    ...orderModule
}

在 store 中引入模块

// 导入home 模块
import homeModule from './modules/home/index'
// 导入order 模块
import orderModule from './modules/orders/index'

export default new Vuex.Store({
  modules: {
    homeModule,
    orderModule
  }
})

至此我们已经完成了基本的模块化,接下来我们看看如何使用它

<template>
    <div>
        <!-- 使用 homeModule 模块中的 state 需要通过 $store.state.homeModule.*** -->
        <h2>{{$store.state.homeModule.username}}</h2>
        <!-- 使用 homeModule 模块中的 getters 通过 $store.getters.*** 不需要带上homeModule模块名-->
        <h2>{{$store.getters.usernameHandle}}</h2>
    </div>
</template>

命名空间

默认情况下,模块内部的 state 是注册在局部命名空间的,而 action、mutation、getter 是注册在全局命名空间,也就是为什么下面这段代码的获取 getters 的时候不需要带上homeModule模块名,而 state 需要带上模块名

// 获取 homeModule 模块中的 state、action、mutation、getter
{{$store.state.homeModule.**}}
{{$store.getters.**}}
{{$store.mutations.**}}
{{$store.actions.**}}

在上述这种情况下,如果模块间出现相同命名则会造成冲突的错误,所以我们可以添加 namespaaced:true 的方式来使其成为带命名空间的模块(也是就是 action、mutation、getter 都会注册时根据其模块名调整命名)。下面我们看看如果使用带命名空间的模块。

先在 orderModule 中加上 namespaced:true

image.png

下面我们在控制台打印一下 getters

image.png 用法如下:

this.$store.getters['orderModule/orderCountHandle']
this.$store.actions['orderModule/orderAction']
this.$store.mutations['orderModule/orderMutation']

注意:模块中嵌套的子模块如果不使用命名空间会默认继承父级的命名空间

rootState、rootGetters 参数

上面我们说到 Getter 中一共有 stategettersrootStaterootGetters 四个参数,前两个参数我们已经说过了,下面在 Module 中我们来说一下这 rootStaterootGetters 两个参数

先修改一下 orders 中的 orderCountHandle 看一下正常的打印信息

 orderCountHandle(state,getters,rootstate,rootgetters){
    console.log("orders中的rootgetters:",rootgetters);
    console.log("orders中的rootstate:",rootstate);
    return rootgetters.getTitle;
}

image.png

根据上面打印出来的数据可以看到,rootStaterootGetters 两个参数分别代表根节点的 stategetters,这样我们就能获取到其他模块中对应的函数或者属性。