前言
Vuex作为Vue官方维护和推荐的全局状态管理插件,是使用Vue框架的前端开发所必须掌握的一个知识点,实际上Vuex的使用也确确实实能够在项目开发中帮助我们处理复杂组件关系间的值传递问题。本篇文章将对Vuex的作用、核心概念以及使用进行介绍,希望对各位有所帮助。
一、Vuex的作用和插件安装
(一)Vuex的作用
在学习Vuex之前,我们不妨先来想一个问题,组件之间的状态(state
)如何进行共享/传递?
我们可能很容易就会想到,父子组件间使用props就可以方便地实现状态共享了。实际上也确实可以,但是如果说想要共享状态的组件并不是简单的父子组件,而是嵌套多层的祖孙组件,又或者是兄弟组件,这个时候我们又可以怎么来解决这个问题呢?
方法自然还是有的,比如我们还可以通过事件总线的方式来解决这个问题,或者是自己维护一个全局对象,来对共享的状态进行管理。这两种方法自然是好的,但是却存在不同的缺点,前者在项目维护的共享状态数量很多时,会让代码的可读性变差(代码中有很多on我们比较难找到某个状态发生变更的时机),而后者的话虽然可以实现状态的统一保存,但是却无法实现响应式的效果。
(二)Vuex的浏览器插件安装
Vue开发团队为了让我们能够更方便地对自己的Vue项目进行调试,提供了一款名为devtools
的浏览器插件给我们使用。我们在chrome的网上应用商店即可完成插件的搜索和安装。
基于上述的原因,在相对复杂的项目开发中,建议还是使用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.$emit
和this.$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>
上面的代码虽然实现了对于共享状态的获取和修改,但是真实开发中,对于共享状态的修改,我们一般并不是直接通过修改this.$store.state.xxx
来实现我们的修改目的的。而是专门交由mutations来实现。下面是Vue官方描述的Vuex作用流程图,我们可以看到,对于state的状态修改,组件一般是要经过Dispatch
、commit
和Mutate
三个流程才能实现的。
如何直接修改state的值,那么可能会导致图中的Devtools
(也就是我们浏览器的Devtools插件)不能检测到state的变化,可能会导致后续我们无法通过Devtools
工具来调试state。
所以我们一般会在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>
案例二:在案例一的基础上,通过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>
(三)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>
需要注意的是,是如果参数不是一个,这个时候, 我们通常会以对象的形式传递, 也就是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;
上面没有实现响应式的原因是,
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封装为常量来使用。具体可以参考下面的代码:
(四)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);
}
}
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返回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);
}
(五)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帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰
对于store目录,Vue官方推荐我们使用index.js来存放唯一的store和其中的根级别state,而根级别的action和mutation则建议我们单独抽取出来,对于多个模块之间的状态,则创建modules目录,在新的目录中再单独管理。
这样就可以在后期让我们的维护变得更加方便。
至此,对于Vuex的使用就介绍完毕,上述知识点也基本上满足实际项目中的开发应用了。对于Vuex的概念,如果有学过Redux的同学来学习,相对来说可能更加容易上手。大家加油☺