Vuex学习
一、Vuex是做什么的?
官方解释:Vuex是一个专为Vue.js应用程序开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种个预测的方式发生变化,Vuex也集成到Vue的官方调试工具devtools extension,提供了诸如零配置的time-travel调试、状态快照导入导出等高级调试功能。
看完官方解释,如果你不理解什么式状态管理,那肯定得一脸懵逼。
-
什么是状态?
状态其实就是一些变量,比如说用户的信息啊,地理位置这些需要在多个页面显示的状态就需要多个组件之间共享这些状态。
-
什么是状态管理?什么是集中式存储管理?
通过我们原先组件间传递变量的方式只能父传子,子传父。当我们的项目变得越来越大,然后形成组件树之后,各个不同分支的组件之间如果想要使用某些相同的数据的话就会非常麻烦,就要一级一级的传。所以就需要进行某些公用状态的统一管理,这就是状态管理,而全部公用的状态交给一个对象进行管理的方式就式集中式管理。就像是公司的财政部,你这边想要用钱,那就找财政部开,那边想要用钱,也就去找财政部开。将公司的财政进行集中式,交给财政部打理,就差不多这样。而Vuex就是扮演了这种大管家的作用,只要是公用的状态都有它来管理,这样就大大的简化了组件间共享数据的传递。
-
为什么需要使用一个专门的创建来进行状态管理?
如果仅仅只是进行状态管理,那感觉好像没必要安装一个专门的插件,咱们自己在Vue的原型对象中添加一个对象,Vue.prototype.objManager = {},不就能进行状态管理了吗?所以,Vuex最大的便利其实和VueJS一样,它们都加入了响应式系统,所以它们中的属性能够做到响应式,所以我们使用Vuex。
二、单页面的状态管理
单个页面就是单个组件,在一个组件中进行状态管理是非常简单的,因为不需要进行组件间数据的传递。其中
State:就是需要管理的状态,就相当于data中的保存的变量。
View:视图层,用来显示State中的数据
Actions:就是一些用户的交互操作,比如说点击啊什么的,然后State会根据Actions来改变状态。
三、多页面的状态管理
Vue中对单个组件中的状态管理已经帮我们实现好了,而且是响应式的。那如果现在我们需要进行多个页面也就是多个组件间都需要使用的一些状态的管理,而且也想要它们做到响应式呢,这就需要使用到vuex了,它就是一个全局的大管家,它是通过单例设计模式设计出来的,由于只会有一个大管家,而且它一直存在,那就可以将共享的状态交给它保存管理,然后按照它的使用规则就能做到响应式的管理共享状态了。
上面提到了vuex的使用规则,那都有哪些规则呢,后面我们再继续学习,现在先记住一点,就是State的改变必须得通过Mutations来进行,上面这幅图就是它的工作流程。如果你在组件中直接来拿到State进行修改的话,Devtools就检测不到数据的改变了。
四、多页面状态管理,vuex基本使用,计数器小案例
想要进行多页面状态的管理,那就需要使用到vuex插件了
Add组件
<template>
<div>
<h2>------------Add的内容-------------</h2>
<button @click="countAdd">+</button>
</div>
</template>
<script>
export default {
name:"Add",
methods: {
countAdd(){
// this.$store.state.count++;不能这么干
this.$store.commit('increment');
}
},
}
</script>
<style scoped>
</style>
你可以下载vue提供的devtools浏览器插件,这个插件用于对vue项目进行debug等等操作,它能监听vuex的state改变,不过必须是要通过mutation改变的才行,你可以通过this.$store.state.count直接来改变state,不过不要这样做,虽然这样也能响应式,不要跳过mutation来修改state的内容。这样devtools是捕捉不到这次修改的记录的,这样子devtools的数据和页面显示的就不同了。
$store和之前的$router差不多,都是代指我们挂载在Vue实例对象中的router或着store
五、Vuex状态管理图例
通过这副vuex的状态管理条例,可以很明确的看出来,State最好通过Mutations来进行修改,官方也推荐这样。
六、Vuex的核心概念
-
State
-
State单一状态树
单一状态树的意思指的是,在我们的应用中只有一个Vuex提供的这个大管家。为什么这样设计呢,就是因为如果我们的状态分别存储在多个store对象中的话,那么如果你某个需求需要用到好几个store里的状态,那么就得分别去多个store里拿,这在之后的管理和维护都会变得非常困难,因为你同时得维护多个store,特别的调试的时候,如果出现了一个bug,可能就得全部store排查一遍。
-
-
getters
-
getters基本使用
getters和我们原先在组件中定义的computed有异曲同工之妙,都是将原有的数据进行一定的变换后返回。
const store = new Vuex.Store({ state:{ goods:[ {id:100,name:'热水壶',price:80}, {id:101,name:'保温杯',price:83}, {id:102,name:'洗衣机',price:2000}, {id:103,name:'热水器',price:134}, {id:104,name:'电视',price:4141}, {id:105,name:'电脑',price:2454}, {id:106,name:'冰箱',price:414}, {id:107,name:'羽绒服',price:4641} ] }
现在需要取出State中价格高于500的商品,这就需要使用到getters了。不可能每个使用到的地方都重复的写一串代码吧,这也是计算属性和getters 的意义。
getters:{ pMFiveH(state) { return state.goods.filter(g => g.price > 500); } }
-
getters本身作为参数
getters本身是可以作为参数,传入某个getter中的,这样子就可以进行一些混合的操作了,接着上面的例子,现在需要求出价格高于500的商品的数量。
getters:{ pMFiveH(state) { return state.goods.filter(g => g.price > 500); }, //注意:这里虽然state没有用到,都是参数列表中一定要先写state,才能写getters,因为它在传参的时候就是先传入的state然后才传的getters pMFiveHLength(state,getters) { return getters.pMFiveH.length; } }
-
getters传递参数
getters和computed一样,默认是不能传递参数的,他们是当作属性一样来使用的而不是方法,如果我们想要往getters中传入参数,那么只能让getters返回一个函数,然后我们调用这个函数。
getGoodById(state,getters,id) { console.log(id); }
如果强行往getters定义的方法中传入第三个你自定义的参数,并且试图调用它的话,它会报这个错误
Error in render: "TypeError: _vm.$store.getters.getGoodById is not a function"
所以验证了不能传参,现在就来解决这个问题
getGoodById(state) { /* find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。 find() 方法为数组中的每个元素都调用一次函数执行: 当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。 如果没有符合条件的元素返回 undefined 注意: find() 对于空数组,函数是不会执行的。 注意: find() 并没有改变数组的原始值。 */ return id => state.goods.find(g => g.id === id) }
可以这样定义方法,然后这样调用
{{$store.getters.getGoodById(100)}}
$store.getters.getGoodById ==> 返回一个函数fn
然后调用这个fn(100)
-
-
Mutations
-
mutations基本使用
上面给计数器小案例已经演示过基本使用了,Vuex的store状态更新的唯一方式:提交Mutation,Mutation主要包括两部分,字符串的事件类型(type),一个回调函数,该回调函数的第一个参数就是state。
mutations:{ increment//1.事件类型,提交的时候就是提交它 (state) { state.count++; },//2.回调函数 decrement(state) { state.count--; } }, this.$store.commit('increment');//提交mutation
-
mutations传递参数
在使用mutation更新状态的时候,我们可能希望携带一些额外的参数,这些参数被称之为mutation的载荷(Payload)
subFive(state,n){ state.count -= n; }
如果只有一个参数,就这样子传过去,不过一般传过去的都是一个对象,这个对象就是载荷payload.
subFive(state,payload){ state.count -= payload.count; } this.$store.commit('subFive',{ count:5 });
-
mutations提交风格
上面的commit进行提交是一种普通的方式,vue还提供了另外的一种提交风格,就是commit一个对象。
this.$store.commit({ type:'subFive', count:5 });
这种提交风格,vue是将提交的对象整体作为Payload来处理,这种风格看起来更加好看一点。
-
mutations响应规则
之前说必须要按照一些Vuex的规则进行数据的改变才能做到响应式,现在就来学习一下都有哪些规则。
提前在store中初始化好所需的属性
-
当给state中的对象添加新属性时,请这样添加
- 使用Vue.set(obj,'newProp',value)
- 将新的对象给旧的对象,就是将旧的对象的指针重新指向新的对象
先试一下直接添加
//定义Mutation aAddHeight(state,payload) { state.author.height = payload.height; } /*************/ //提交Mutation addHeight(){ this.$store.commit({ type:'aAddHeight', height:1.88 }) }
-
非常尴尬,明明state已经改变了,但是页面没有改变,这是因为Vue中有个响应式系统,在将Vuex的store挂载到Vue实例前,vue会将所有已经定义的属性给加入到响应式系统中,而原来没有的属性自然不会有响应式了,因为它没有加入到响应式系统中。那怎么将没有添加入响应式系统的属性给手动的添加进去呢,这就是Vue.set方法,它会将属性给加入响应式系统内。
> set(object: object, **key: string | number**, value: any): any
aAddHeight(state,payload) {
//无法响应式
// state.author.height = payload.height;
//响应式
// 方式一:使用Vue.set
// Vue.set(state.author,'height',payload.height);
//方式二:将旧对象重新赋值
//...state.author将这个对象解构出来,然后放在这里
state.author = {...state.author,'height':payload.height}
}
这下子就能给新附加的属性也加入到响应式系统中了。
如果说Vue.set可以将之前没定义的属性加入到响应式系统中,那旧对象重新赋值为什么也能响应式呢,我的理解是,虽然state中的旧author对象中原来没有height属性,但是state中原先是定义了一个author,这个对象是响应式的,所以这个对象整体的改变就是响应式的。
-
mutations常量管理
前面我们说过一个Mutation又一个事件类型和一个回调函数构成。在提交mutation的时候是要提交这个事件类型,当我们的项目越来越大的时候,Vuex管理的状态越来越多,需要更新的状态也会越来越多,所以mutation就会被定义的越来越多。
方法多了之后,使用者在使用这些Mutation的时候,直接写嘛,容易单词拼写错误,返回去复制嘛又浪费时间,这时候就可以使用常量来替代Mutation的事件类型,这样就方便管理这些事件类型了,而且还能让整个项目的事件类型一目了然,就是没有了业务代码掺杂在一起了。
具体怎么做呢?
创建一个文件:mutation-types.js并且在其中定义我们需要使用的事件类型常量
-
在使用这些常量命名函数是可以这样来使用
[常量]() {
函数体
}
mutation-types.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const SUB_FIVE = 'subFive'
export const A_ADD_HEIGHT = 'aAddHeight'
- mutations
import * as types from './mutation-types.js'
...
mutations:{
[types.INCREMENT](state) {
state.count++;
},
[types.DECREMENT](state) {
state.count--;
},
[types.SUB_FIVE](state,payload) {
state.count -= payload.count;
},
[types.A_ADD_HEIGHT](state,payload) {
state.author = {...state.author,'height':payload.height}
}
},
- 使用
import {A_ADD_HEIGHT} from 'store/mutation-types.js'
methods: {
addHeight(){
this.$store.commit({
type:A_ADD_HEIGHT,
height:1.88
})
}
},
-
mutations同步函数
若是想要使用devtools的话,还有一个要求,就是在Mutation中的方法必须是同步方法,如果在Mutation中又异步操作的话,devtools将不能很好的追踪这个操作什么时候会被完成。
[types.A_ADD_HEIGHT](state,payload) { //使用setTimeout测试异步操作 setTimeout( () => { state.author = {...state.author,'height':payload.height} },1000); }
你会发现,虽然页面改变了,说明state的值已经修改了,但是devtools中的author对象却没有追加一个height属性,因为它没有追踪到mutation的完成。所以不要在mutation中进行异步操作。
-
Actions
上面我们说到在mutation中不能进行异步操作,那如果我们确实不能再Vuex中进行一些异步操作怎么办呢,比如说一些网络请求。这个时候就可以使用Vuex图例中的Action了.
Action和Mutation类似,是用来代替Mutation进行异步操作的。Mutation默认会传入state,可以传入payload。而Action默认会传入context,上下文对象,具有和store对象相同的方法和属性。不过他们并不是同一个对象。
-
基本使用
/*********定义**************/ actions:{ [types.A_ADD_HEIGHT](context,payload) { setTimeout(() => context.commit({ type:types.A_ADD_HEIGHT, height:payload.height }),3000); } } /********使用*********/ methods: { addHeight(){ this.$store.dispatch(A_ADD_HEIGHT,{ height:1.88 }); } },
Action是使用dispatch来使用的,和Mutation一样可以携带一个对象payload
//也有这种使用风格 this.$store.dispatch({ type:A_ADD_HEIGHT, height:1.88 });
-
- Action返回Promise
有时我们希望再进行网络请求成功之后进行某些操作,那么我们如何直到异步操作已经结束了呢,我们可以让Action返回一个Promise。然后在组件中then,这样在异步操作完成之后调用resolve然后就会执行then中的代码。
/***************定义*************/
actions:{
[types.A_ADD_HEIGHT](context,payload) {
return new Promise((resolve,reject) => {
setTimeout(() => context.commit({
type:types.A_ADD_HEIGHT,
height:payload.height
}),3000);
resolve("网络请求已经完成了");
})
}
}
/***************使用*************/
methods: {
addHeight(){
this.$store.dispatch({
type:A_ADD_HEIGHT,
height:1.88
}).then(res => {
console.log(res);
})
}
},
-
Modules
-
认识Module
官方说法:
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
const moduleA = { state:{ count:7 } } const moduleB = { state:{ count:5 } } //创建store对象 const store = new Vuex.Store({ state:{ count:0 }, modules:{ a:moduleA, b:moduleB } })
通过$store.state.a的方式拿到moduleA的state
通过$store.state.b的方式拿到moduleB的state
虽然我们定义的store里的state里没有a,但是它最终会把Modules里的东西放到store的state里面的。
-
-
默认情况下的Modules
默认情况下,也就是上面这种定义Modules的方式,这些模块是被注册在全局命名空间里的,这就相当于只是将原本庞大的store对象给切割了,分开保存而已,模块和store对象之间还是非常紧密联系的。
这种情况下,所有模块中定义的Mutations和Actions都会接收来自全局commit的事件类型。就像是他们还像原来那样定义一样,不过这时候Mutation和getter接收的第一个参数是模块的局部状态对象,就是哪个模块的Mutation和getter就针对这个模块进行操作,这样子使得我们更好的对状态进行管理。
const moduleA = { state:{ count:0 }, mutations:{ increment(state){ state.count++ } } } const moduleB = { state:{ count:0 }, mutations:{ increment(state){ state.count++ } } } //2.创建store对象并且导出 export default new Vuex.Store({ state:{ count:0 }, mutations:{ increment(state){ state.count++ } }, modules:{ a:moduleA, b:moduleB } });
这时候提交一个increment事件,三个mutation都会收到这个事件类型,然后做出操作。如果在模块中定义了相同名字的getter会直接报错的,说getter重复了。
对于模块内部的getter,根节点状态会作为第三个参数暴露出来
getter(state,getters,rootState)
对于模块内部的action,它接收的第一个参数是依然是上下文对象context,这个对象中有这些属性。
里面的commit,dispatch,getters都是全局的,state是局部的
所以根据上面的探讨,结论是,在使用这种默认的Mudules来进行分模块状态管理的时候,不要出现重名的Mutation,getter,Action
-
给模块添加上命名空间
如果的确是想要使用重名的Mutation,getter,Action的话,那就得将单个的模块给封装起来了,就是给他加上命名空间,添加一个属性namespaced:true,以后它里面的状态管理,提交mutation就得这样commit('a/xxxx'),其中a是模块名,xxxx是事件类型。
-
文件目录的组织
官方文档有这么一句话:
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例
Vuex推荐我们这样来组织文件目录
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块