Vuex的使用

前言

Vuex作为Vue官方维护和推荐的全局状态管理插件,是使用Vue框架的前端开发所必须掌握的一个知识点,实际上Vuex的使用也确确实实能够在项目开发中帮助我们处理复杂组件关系间的值传递问题。本篇文章将对Vuex的作用、核心概念以及使用进行介绍,希望对各位有所帮助。

一、Vuex的作用和插件安装

(一)Vuex的作用
在学习Vuex之前,我们不妨先来想一个问题,组件之间的状态(state)如何进行共享/传递?

我们可能很容易就会想到,父子组件间使用props就可以方便地实现状态共享了。实际上也确实可以,但是如果说想要共享状态的组件并不是简单的父子组件,而是嵌套多层的祖孙组件,又或者是兄弟组件,这个时候我们又可以怎么来解决这个问题呢?
方法自然还是有的,比如我们还可以通过事件总线的方式来解决这个问题,或者是自己维护一个全局对象,来对共享的状态进行管理。这两种方法自然是好的,但是却存在不同的缺点,前者在项目维护的共享状态数量很多时,会让代码的可读性变差(代码中有很多emit和on我们比较难找到某个状态发生变更的时机),而后者的话虽然可以实现状态的统一保存,但是却无法实现响应式的效果。

(二)Vuex的浏览器插件安装

Vue开发团队为了让我们能够更方便地对自己的Vue项目进行调试,提供了一款名为devtools的浏览器插件给我们使用。我们在chrome的网上应用商店即可完成插件的搜索和安装。

安装devtools用例1

安装devtools用例2

基于上述的原因,在相对复杂的项目开发中,建议还是使用Vuex来作为共享状态管理的工具,当然了若是简单的父子组件状态传递,则还是使用Prop更加方便一些。

二、Vuex的简单使用

下面我们将使用Vuex来完成一个小案例,我们先在vue-cli中创建一个简单的组件并放入App组件中
App组件

<template>
  <div id="app">
    <h2>---这里是App组件----</h2>
    {{childNum}}
    <couter @add="add" @reduce="reduce"/>
  </div>
</template>

<script>
import couter from './components/Couter'
export default {
  name: 'App',
  data(){
    return {
      childNum:0
    }
  },
  components: {
    couter
  },
  methods:{
    add(num){
      this.childNum = num;
    },
    reduce(num){
      this.childNum = num;
    }
  }
}
</script>

Couter组件

<template>
    <div>
        <h2>我是Couter组件</h2>
        <div>
            {{counter}}
        </div>
        <button @click="increment">加</button>
        <button @click="decrement">减</button>
    </div>
</template>
<script>
export default {
    name: 'couter',    
    data(){
        return {
            counter:0
        }
    },
    methods:{
        increment(){
            this.counter++;
            this.$emit('add',this.counter);
        },
        decrement(){
            this.counter--;
            this.$emit('reduce',this.counter)
        }
    }
}
</script>

使用原生方式实现状态共享

我们可以看到,在上面的案例中,由于App组件和Couter组件之间是父子组件的关系,所以我们采用了this.$emitthis.$on的方式来进行状态的共享。如果我们使用Vuex的方式来实现,我们可以怎么做呢?

步骤一:安装Vuex
npm install vue --save
步骤二:在vue实例中注册vuex插件

在src目录下创建store文件夹,在store文件夹中创建index.js文件,文件中对Vuex插件进行注册,且导出store对象。

import Vue from 'vue';
import Vuex, { Store } from 'vuex';
//使用Vuex插件
Vue.use(Vuex);

// 定义store对象
const store = new Vuex.Store({
    state:{
        counter: 0
    }
})

//导出store对象
export default store;
步骤三:把store对象挂载到Vue实例中

main.js文件中,把store对象进行挂载,使得所有的Vue组件都可以使用store对象

import Vue from 'vue'
import App from './App'
import store from './store'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
步骤四:修改App.js和Couter组件的代码
  • App组件代码
<template>
  <div id="app">
    <h2>---这里是App组件----</h2>
    {{$store.state.counter}}
    <couter/>
  </div>
</template>

<script>
import couter from './components/Couter';
export default {
  name: 'App',
  data(){
    return {
      childNum:0
    }
  },
  components: {
    couter
  }
}
</script>
  • Couter组件代码
<template>
    <div>
        <h2>我是Couter组件</h2>
        <div>
            {{$store.state.counter}}
        </div>
        <button @click="increment">加</button>
        <button @click="decrement">减</button>
    </div>
</template>
<script>
export default {
    name: 'couter',
    methods:{
        increment(){
            this.$store.state.counter++;
        },
        decrement(){
            this.$store.state.counter--;
        }
    }
}
</script>
使用Vuex进行状态管理演示

上面的代码虽然实现了对于共享状态的获取和修改,但是真实开发中,对于共享状态的修改,我们一般并不是直接通过修改this.$store.state.xxx来实现我们的修改目的的。而是专门交由mutations来实现。下面是Vue官方描述的Vuex作用流程图,我们可以看到,对于state的状态修改,组件一般是要经过DispatchcommitMutate三个流程才能实现的。
如何直接修改state的值,那么可能会导致图中的Devtools(也就是我们浏览器的Devtools插件)不能检测到state的变化,可能会导致后续我们无法通过Devtools工具来调试state。

vuex作业流程图

所以我们一般会在store中定义好修改state的方法:

...
// 定义store对象
const store = new Vuex.Store({
    state:{
        counter: 0
    },
    mutations:{
        increment(state){
            state.counter++;
        },
        decrement(state){
            state.counter--;
        }
    }
})

//导出store对象
export default store;

然后在需要改变共享状态的组件中,使用this.$store.commit()的方式,进行状态的修改。在这个案例中,我们需要修改的组件是Couter组件的代码。

<template>
    <div>
        <h2>我是Couter组件</h2>
        <div>
            {{$store.state.counter}}
        </div>
        <button @click="increment">加</button>
        <button @click="decrement">减</button>
    </div>
</template>
<script>
export default {
    name: 'couter',
    methods:{
        increment(){
            this.$store.commit('increment');
        },
        decrement(){
            this.$store.commit('decrement');
        }
    }
}
</script>

三、Vuex中的核心概念

(一)单一状态树

Vuex提出使用单一状态树, 什么是单一状态树呢?
单一状态树的英文名称是Single Source of Truth,也可以翻译成单一数据源。
单一状态树的概念怎么理解呢?其实就是只维护一个store对象,我们可以想象一下,如果我们根据项目需要,在多个功能模块中使用了不同的store对象,这样虽然写代码的时候觉得很方便,但是对于后续的维护却十分的不利,多个store对象会增加我们操作不同模块下的共享状态的复杂度。
所以Vue官方更加推荐我们只定义和使用一个store对象,通过store对象中的module来区分模块,从而方便共享状态的管理,更好的降低开发难度。

(二)Getter

我们在之前的入门演示中,使用this.$store.state.xxx来直接显示store中存储的共享状态,但如果说对于共享状态我们并不想直接使用,而是希望每次获取的值都是已经经过一定规则转化之后的结果。此时,我们就可以使用store中的getters来解决这个问题。本质上,getters是获取state中某个状态加工后的结果。

案例一:使用Getters属性获取store.state中年龄大于20岁的person:
  • store代码
...
// 定义store对象
const store = new Vuex.Store({
    state:{
        counter: 0,
        persons:[
            {id:'001',name:'jim',age:15},
            {id:'002',name:'amy',age:22},
            {id:'003',name:'vivi',age:14},
            {id:'004',name:'cookie',age:24},
        ]
    },
    mutations:{
       ...
    },
    getters:{
        getGT20Persons(state){
            return state.persons.filter(e => e.age>20);
        }
    }
})

//导出store对象
export default store;
  • 使用getter的组件代码
<template>
    <ul>
        <li v-for="item in $store.getters.getGT20Persons" :key="item.id">{{item.name}}</li>
    </ul>
</template>
<script>

export default {
    name:'Person'
}
</script>
getters演示结果1
案例二:在案例一的基础上,通过getter创建可以指定具体限制年龄条件的方法

这里的话主要是演示getters传递参数的使用方式,主要是使用柯里化函数的方式来将原有函数的返回值从数组改为函数 -> 数组

...
// 定义store对象
const store = new Vuex.Store({
    state:{
        counter: 0,
        persons:[
            {id:'001',name:'jim',age:15},
            {id:'002',name:'amy',age:22},
            {id:'003',name:'vivi',age:14},
            {id:'004',name:'cookie',age:24},
        ]
    },
    mutations:{
...
    },
    getters:{
        getGT20Persons(state){
            return state.persons.filter(e => e.age>20);
        },
        getPersonsByAge(state){
            return (age)=>{
                return state.persons.filter(e=> e.age>age);
            }
        }
    }
})

//导出store对象
export default store;
  • person组件代码
<template>
    <div>
    <ul>
        <li v-for="item in $store.getters.getGT20Persons" :key="item.id">{{item.name}}</li>
    </ul>
    <h2>下面是年龄大于14岁的人</h2>
    <ul>
        <li v-for="item in $store.getters.getPersonsByAge(14)" :key="item.id">{{item.name}}</li>
    </ul>
    </div>
</template>
<script>

export default {
    name:'Person'
}
</script>
getters演示用例2
(三)Mutation
使用Mutation进行状态更新

Vuex的store状态的更新唯一方式:提交Mutation。Mutation主要包括两部分:

(1)字符串的事件类型(type)
(2)一个回调函数(handler),该回调函数的第一个参数就是state。

我们在入门案例中,其实就已经使用过Mutation进行过简单场景下的状态更新了,具体分为两个步骤:

步骤一:定义Mutation函数
mutations:{
        increment(state){
            state.counter++;
        },
    }
步骤二:使用mutation进行更新
this.$store.commit('increment');
使用Mutation进行参数传递

在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数,参数被称为是mutation的载荷(Payload)

步骤一:定义Mutation函数
mutations:{
      ...
        updateNum(state,n){
            state.counter = n;
        }
    },
步骤二:使用mutation进行更新
<template>
    <div>
        <h2>我是Couter组件</h2>
        <div>
            {{$store.state.counter}}
        </div>
        <button @click="increment">加</button>
        <button @click="decrement">减</button>
        <button @click="add5">值变为5</button>
    </div>
</template>
<script>
export default {
    name: 'couter',
    methods:{
       ...
        add5(){
            this.$store.commit('updateNum',5);
        }
    }
}
</script>
mutation演示用例1

需要注意的是,是如果参数不是一个,这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象。

Mutation的提交风格

mutation除了上面的提交方式外,还可以使用另外一种风格来进行提交:

add5(){
            this.$store.commit({
                type:'updateNum',
                count:5
            });
        }

函数定义方式如下:

        updateNum(state,n){
            state.counter = n.count;
        }
Mutation的响应规则

Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新。但是Vuex的响应式生效的前提是我们在使用过程中遵循了Vue的规则:
(1)提前在store中初始化好所需的属性
(2)当给state中的对象添加新属性时, 使用下面的方式:
方式一: 使用Vue.set(obj, 'newProp', 123)
方式二: 用新对象给旧对象重新赋值
其实上面的两个规则也是组件中data属性实现响应式的前提。我们可以看一下下面这个案例:

<template>
  <div id="app">
...
  {{$store.state.student}}
  <button @click="updateStu">点击更新stu信息</button>
  </div>
</template>

<script>
...
export default {
  name: 'App',
  data(){
    return {
      childNum:0
    }
  },
  components: {
   ...
  },
  methods:{
    updateStu(){
      this.$store.commit({
        type:'updateStu',
        height: 188
      })
    }
  }
}
</script>
  • store代码
const store = new Vuex.Store({
    state:{
       ...
        student:{name:'Jimy',age:15}
    },
    mutations:{
        ...
        updateStu(state,payload){
            state.student['height'] = payload.height;
        }
    },
    getters:{
      ...
    }
})

//导出store对象
export default store;

Mutation未实现响应式演示

上面没有实现响应式的原因是,height没有在初始化的student对象中,属于是新加上去的属性,所以并没有进入Vue的响应式管理中,如果想要实现响应式的话,需要根据上面提到的三种方式任取其一来进行解决。下面列举其中两个方法,都是通过修改Matation来解决的。

 //方式一
state.student = {...state.student,'height':payload.height}
//方式二
Vue.set(state.student,'height',payload.height);

其实如果各位读者对于Redux有所了解的话,会发现其实Vuex中的Mutation和Redux中的Reducer是差不多的,都是作为对共享状态进行修改的工具。同时,为了书写方便,我们一般会将Mutation中的type封装为常量来使用。具体可以参考下面的代码:

新建mutation-type.js文件

在Mutation方法中使用常量

在具体的组件中使用常量来修改共享状态

(四)Action的使用
Action的概念和使用场景

Vuex中的action和Redux中的actionCreator虽然名称相似,但是其使用的场景却有所差异。我们知道Redux中的actionCreator主要是用于生产action事件类型对象,而Vuex中并没有把事件类型对象独立出来让我们去自行封装,而是把Action作为Mutation的替代品,代替Mutation进行异步操作。
因为通常情况下, Vuex要求我们Mutation中的方法必须是同步方法,主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照。 但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.。

    mutations:{
        [type.UPDATE_STU](state,payload){
            setTimeout(()=>{
                Vue.set(state.student,'height',payload.height);
            },1000);
        }
    }
Mutation中不适合完成异步动作
Action的定义

我们强调, 不要再Mutation中进行异步操作,但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的, 这个时候就需要使用Action来解决?
Action类似于Mutation, 但是是用来代替Mutation进行异步操作的

  • store代码
mutations: {
        [type.UPDATE_STU](state, payload) {
            Vue.set(state.student, 'height', payload.height);
        }
    },
 actions: {
      [type.UPDATE_STU](context, payload) {
         setTimeout(() => {
              context.commit(type.UPDATE_STU,payload)
         }, 1000);
    }
 }
  • 具体的组件代码
  methods:{
    updateStu(){
      this.$store.dispatch({
        type: UPDATE_STU,
        height: 188
      })
    }
  }
使用Action完成异步动作
Action返回Promise对象

在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.

  • store代码
actions: {
        [type.UPDATE_STU](context, payload) {
            return new Promise(resolve => {
                setTimeout(() => {
                    context.commit(type.UPDATE_STU,payload)
                }, 1000);
                resolve('aaa');
            })
        }
    },
  • 具体的组件代码
    updateStu(){
      this.$store.dispatch({
        type: UPDATE_STU,
        height: 188
      }).then(console.log);
    }
action返回Promise对象演示用例
(五)Module的使用

Module是模块的意思, 为什么在Vuex中我们要使用模块呢?
原因是由于Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutations、actions、getters等。需要注意的是,对于模块中的state、mutation\actions和getters,我们在使用上是略有差异的。(主要差异在于使用时是否加上模块名)

  • store代码
const moduleA = {
    state:{
        name: 'xiaoming'
    },
    mutations:{
        changeName(state){
            state.name = 'xiaohong'
        }
    },
    getters:{
        getUppercaseName(state){
            return state.name.toUpperCase();
        }
    }
}
// 定义store对象
const store = new Vuex.Store({
    state: {
        ...
    },
    modules:{
        a:moduleA
    }
}
  • 具体的组件代码
<template>
  <div id="app">
  {{$store.state.a.name}}
  {{$store.getters.getUppercaseName}}
  <button @click="changeName">点击换名</button>
  </div>
</template>
<script>
...
export default {
  name: 'App',
  ..
  methods:{
  ...
    changeName(){
      this.$store.commit('changeName');
    }
  }
}
</script>

我们可以看到,对于模块中的state值,我们需要通过this.$store.state.模块名.属性来获取,但是对于模块中的Getters和Mutations,我们只要和正常一样使用就行。

四、Vuex的目录结构

当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰


Vuex的目录结构

对于store目录,Vue官方推荐我们使用index.js来存放唯一的store和其中的根级别state,而根级别的action和mutation则建议我们单独抽取出来,对于多个模块之间的状态,则创建modules目录,在新的目录中再单独管理。
这样就可以在后期让我们的维护变得更加方便。

至此,对于Vuex的使用就介绍完毕,上述知识点也基本上满足实际项目中的开发应用了。对于Vuex的概念,如果有学过Redux的同学来学习,相对来说可能更加容易上手。大家加油☺

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容

  • 在 vue 开发中,组件通信一直是一大痛点。 当项目是很简单的 SPA 或者多入口项目时,可以靠着 vue 自带的...
    若年阅读 1,672评论 0 4
  • vuex是一个专门为vue构建的状态管理工具,主要是解决多组建状态共享的问题。强调的是集中式管理(组件与组件之间的...
    离陌Study阅读 8,379评论 0 4
  • 配置 vuex 和 vuex 本地持久化 目录 vuex是什么 vuex 的五个核心概念State 定义状态(变量...
    稻草人_9ac7阅读 881评论 0 5
  • Vuex是什么? 按vuex官网的说法,vuex是专为vue.js开发的状态管理模式,它采用集中式存储管理应用的所...
    千里一线缘阅读 473评论 0 0
  • 1.简介 1.1Vuex是什么 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 Vuex其实是管...
    大麦茶1024阅读 422评论 0 2