先埋一个伏笔
看不懂不要紧。这张图片只是一个伏笔
前奏
学习一个新的知识,应该带有一些目的性,或者了解一下相关背景,带着疑问去学一个东西,至少不会那么痛苦。(不要跟我说学习是快乐的,学习哪来的快乐( ̄ー ̄) ( ̄ー ̄))
我们先看一个场景,一个很常见且简单的需求。
有2个兄弟组件A,B,现在A,B组件有一个count
变量需要共享,也就是count
改变了,两边组件的count
都要改变,这时候除去我们知道的vuex
,我们可以怎么做呢
1.使用*总线$bus
2.把count放到A,B组件的父级组件上,然后父子组件传参
3.借用本地存储
思路肯定是没有问题的,问题也能够解决,但是,如果现在的场景是A,B,C三个组件(甚至是三个以上组件)都需要共享同一个变量呢?还用以上2种方法就显得非常麻烦和累赘了。带着这个问题,我们看看应该怎么处理。这时候就有同志问了,那为什么不借用本地存储呢?别急,继续往后看
主题
接下来介绍我们的主角:vuex
vuex
官方称为vue的状态管理工具,通俗一点解释,就是一个大仓库,项目中的所有组件都可以向vuex
这个大仓库拿东西,删东西,改东西。
但是需要遵循一定的游戏规则。并且仓库的东西被动了,会通知到所有的组件:’啊,我的东西被别的组件给修改了‘。所以vuex
的数据是响应式的。这就是本地存储跟vuex
很大的一个区别
先上用vue-cli4搭出来的store
目录下index.js
文件的脚手架代码,先说一下Vue的原型上是有$store
属性的,这个可以认为是vuex
仓库
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
我们看到vuex一共有5个模块,我们一一来进行分析
State
state
就是我上面所说的大仓库,所有的组件都可以拿到state
里面的数据,再次强调一下,state里面的数据是响应式的。
我们在state
里面搞一个变量count
export default new Vuex.Store({
state: {
count: 1
},
mutations: {
},
actions: {
},
modules: {
}
})
讲了这么多,我们开始上A组件的代码
<template>
<div class="a-container">
<h3>我是A组件</h3>
{{ $store.state.count }}
</div>
</template>
<script>
</script>
<style scoped lang="less"></style>
在上B组件的代码
<template>
<div class="b-container">
<h3>我是B组件</h3>
{{ $store.state.count }}
</div>
</template>
<script>
export default {
name: 'B',
data () {
return {}
},
}
</script>
<style scoped lang="less"></style>
在上视图
很明显,state
的值我们都拿到了。但是这时候问题又来了,如果一个组件或多个组件都需要使用这个count
,那么我们在模板页面就要反复使用$store.state.count
,觉不觉得点的有点麻烦?这时候我们又来了一个新东西,mapState辅助函数,这个辅助函数就是用来帮我们简化的我们操作的。
1.mapState辅助函数
mapState
是vuex
的辅助函数,主要用来帮助我们在开发中简化我们的操作,更方便的取到state
中的值
1.1mapState辅助函数的数组映射
话不多说,直接上代码
talk is cheap,show you my code
<template>
<div class="b-container">
<h3>我是B组件</h3>
{{ count }}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'B',
computed: {
...mapState(['count'])
},
}
</script>
<style scoped lang="less"></style>
这个地方我暂时还没找到比较好的解释方法,因为我觉得看代码足以能理解mapState
函数怎么使用了。等我想到通俗易懂的语言来解释此处时,我会在更新文档。
1.2 mapState辅助函数的对象式映射
这一部分你需要先了解modules对象的相关知识
,如果你还不了解modules
模块,你等会在来看就比较好了,比如说modules模块有A,B个模块,也就是下面这样
state: {
count: 1
},
mutations: {},
actions: {},
modules: {
A,
B
}
那这时候你再用数组的方式把vuex
的数据映射到当前组件,就肯定不行了,因为当前组件搞不清楚你这个count
是哪个模块里面的了。
这时候就要用到对象式映射了
上代码
<template>
<div class="a-container">
<h3>我是A组件</h3>
{{ count }}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'A',
data () {
return {}
},
props: {},
components: {},
computed: {
...mapState({
// 拿到state模块中的A分支,在取A分支中的count
count: (state) => state.A.count
})
},
}
</script>
<style scoped lang="less"></style>
这一部分我觉得看我代码中的注释完全是能够理解的。
getters
getters
属性可以理解为组件里面的计算属性,举个例子,你去水果店买苹果,最后你的总价,肯定是单个苹果的价格乘以苹果的总数量。
这时候就可以给state
对象里面来2个属性了
export default new Vuex.Store({
state: {
// 苹果的数量
count: 1,
// 苹果的单价
applePrice: 5
},
mutations: {},
getters: {
},
actions: {},
modules: {
A,
B
}
})
那么苹果的总价appleSum
就是count * applePrice
了,这时候苹果的总价格是随着count
和applePrice
这2个变量而改变的,所以这个变量我们就可以放在getters
里面了
state: {
count: 1,
applePrice: 5
},
mutations: {},
getters: {
// 苹果的总价格,这里箭头函数能接受一个参数,就是state,第二个参数是getters,也就是说你可以用getters里面的其他变量
appleSum: (state) => {
return state.count * applePrice
}
},
actions: {},
modules: {
A,
B
}
这里引用Vuex官方的一句话getters 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
在后面的使用中,在组件中使用vuex
里面的值或者函数,统一使用函数映射的方式,这是目前项目中的主流方式。
组件使用getters
中的变量,注意看注释
<template>
<div class="a-container">
<h3>我是A组件</h3>
<h4>我是state里面的值:</h4>
{{ count }}
<h4>我是getters里面的值</h4>
{{ appleSum }}
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
name: 'A',
data () {
return {}
},
props: {},
components: {},
computed: {
...mapState({
count: (state) => state.A.count
}),
// 这种写法你可理解为,把getters中的appleSum映射到当前组件中,并且在当前组件中命名为appleSum
...mapGetters(['appleSum'])
// 如果你想给予其它命名,你可以使用如下写法
...mapGetters({
// key是你自己另外取的名字,value是getters里面的名字
apple_sum: 'appleSum'
})
},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less"></style>
上视图
getters传参
先看场景: 现在你在网上买了苹果付款总价应该是苹果的个数乘以苹果价格。但是现在水果店还有橘子卖。这时候就得根据水果的id
查出水果的价格,在算。这里我们假设用户只能购买一种水果(别杠,理解为主o(╯□╰)o)
上state
里面的变量
state: {
appleCount: 1,
orangeCount: 2,
// 水果信息
fruitInfo: [
{ id: 1, price: 10 },
{ id: 2, price: 20 }
]
}
上getters
里面的变量
getters: {
fruitPrice: (state) => {
return (id) => {
const fruit = state.fruitInfo.find((val) => val.id === id).price
return fruit.price * fruit.count
}
}
},
A组件调用:
<template>
<div class="a-container">
<h3>我是A组件</h3>
<h4>我是state里面的值:</h4>
{{ count }}
<h4>我是getters里面的值</h4>
{{ fruitPrice(2) }}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'A',
data () {
return {}
},
props: {},
components: {},
computed: {
...mapState({
count: (state) => state.A.count
}),
fruitPrice () {
// 计算属性传参
return (id) => {
return this.$store.getters.fruitPrice(id)
}
}
},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less"></style>
上视图
Mutations
还是先说场景,现在用户在某app购买界面买苹果,点击增加的图标,所购苹果的数量肯定要加1,假设这个苹果的数量我们是存在vuex里面的,那么现在就涉及到vuex里面数据的改变了。
你所有对vuex
里面的变量进行的修改操作,都必须通过mutations
这一个对象,Mutations
你可以理解为仓库的管理员,对仓库里面的材料进行改动,你就要告知他。
我们现在mutations
对象里面搞一个对苹果数量增加的操作:
mutations: {
// 操作函数接收2个参数,一个是state容器,一个是你调这个函数所传的参数,传参有2中方式一种是对象式的传参(官方推荐,变量名称清晰明朗)
increnment (state, payload) {
state.appleCount = payload.num + state.appleCount
}
// 另一种是单个变量传参
increnment (state, num) {
state.appleCount = state.appleCount + num
}
}
然后在A组件我们调用一下(还是直接用辅助函数的形式去映射,注意看注释)这里我们跳过this.$store.commit
的方式讲解,因为项目开发这种方式基本不用
<template>
<div class="a-container">
<h3>我是A组件</h3>
<h4>我是state里面的值:</h4>
{{ count }}
<br />
<button @click="onIncreatment">增加</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
name: 'A',
data () {
return {}
},
props: {},
components: {},
computed: {
...mapState({
count: (state) => state.appleCount
})
},
watch: {},
created () {},
mounted () {},
methods: {
// 注意这个辅助函数要放在methods里面
...mapMutations(['increment']),
// 如果你想修改映射过来的函数名称,你可以用对象式写法
...mapMutations({
incre: 'increment'
}),
onIncreatment () {
this.increment({
num: 2
})
}
}
}
</script>
<style scoped lang="less"></style>
视图更新前
点击增加按钮后
视图更新,并且调试工具记录下来了这么一个改变的过程。
关于使用mutations的小细节
-
所有的异步操作,不要放在
mutations
里面,也就是说mutations
里面只能放同步操作。这里要解释一下,你在mutations里面放异步操作不是说代码会无效,而是调试工具它会监测不到数据的改变,假设别人接收你的项目,然后用调试工具调试,那么麻烦就大了。
-
可以使用常量来代理事件类型,也就是说,你的所有
mutations
的命名可以封装起来,这里我项目中暂时还没用到,等实际用到我会在此补充先贴一个官方解释
Actions
如果你有异步操作,比如请求后端接口,那么你可以选择放在actions
模块里面。
场景: 用户登录,登录成功,保存用户数据,登录失败,给予用户提示
actions模块里面的函数也能接受2个参数,第一个是上下文context,官方说跟store是有区别的,关于context和store的区别,我理解之后再更新文档,这里附上context对象
actions
代码:
actions: {
/**
login为已经封装好的登录方法,这里假设用户登录失败后端直接返回错误的状态码
**/
onLogin (context, data) {
return new Promise((resolve, reject) => {
login(data)
.then(res => {
context.commit('setUser', res)
resolve()
})
.catch(err => {
reject(err)
})
})
},
// 如果你经常需要用到commit,你可以直接在接受的时候解构
onLogin ({ commit }, data) {
return new Promise((resolve, reject) => {
login(data)
.then(res => {
// 登录成功把数据存到vuex
context.commit('setUser', res)
resolve()
})
// 登录失败把错误丢出去
.catch(err => {
reject(err)
})
})
}
},
A组件代码,我们还是用辅助函数把actions
里面的函数映射到A组件
<template>
<div class="login-container">
<div>
<button>
登录
</button>
</div>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Login',
data () {
return {
// 后端返回的用户信息
userInfo: null,
}
},
methods: {
...mapMutations(['setUser']),
...mapActions(['onLogin']),
},
// 表单校验成功登录操作
async login () {
try {
await this.onLogin(this.loginForm)
this.$message.success('登录成功')
// 登录成功跳转首页
this.$router.push('/layout/home')
this.isLogin = false
} catch (error) {
const { response } = error
if (response && response.status === 400) {
this.$message.error('手机号或验证码不对')
} else if (response && response.status === 403) {
this.$message.error('权限不足无法登录')
} else if (response && response.status === 507) {
this.$message.error('数据异常')
} else {
// 处理其他异常
}
}
},
}
</script>
<style lang="less" scoped>
</style>
组合Action
场景: 有时候会有这种需求,先根据用户名获取用户id
(假设这个用户id
是好几个组件共享的),在根据用户id
去查找用户相关信息,这时候异步操作就是有顺序的了
上代码:
async onGetUserId ({ commit }, userName) {
const id = await getUserId(userName)
commit('setUserId', {
id
})
},
async onGetUserInfo ({ commit, dispatch, state }, userName) {
// 先等onGetUserId方法完成
await dispatch('onGetUserId', userName)
const userInfo = await getUserInfo(state.userId)
commit('setUserInfo', userInfo)
}
关于伏笔的解释
写到这里,文章最开始的伏笔就起到作用了
这里再次把文章最开始的图片贴出来
我们从Vue Components
开始,假设我们要修改vuex里面的值,如果没有异步操作,那么就通过commit的方式提交给mutations
,devtools
vuex调试工具这时候会监测到该次修改,然后state
里面的值发现改变,相关的组件变量重新渲染,视图得以更新。
如果有异步操作,我们先进行异步操作,根据后端接口返回的不同状态,决定commit
到哪个方法
Modules
还是先说场景,比如我们现在多人协作开发一个商城项目,商城有首页,登录页,商品详情页,搜索页,如果把所有页面需要共享的数据全放在vuex
的state
里面,可以当然是可以,但是容器就变得比较臃肿了,这时候我们就可以进行模块化的区分了,也就是可以把首页的相关组件主要共享的变量拆出来,搜索页需要共享的组件拆出来。这样整个容易的结构就很清晰了,也利于维护。
上目录结构:
store
modules
--home.js
--search.js
上home
和search
页的代码
home.js
export default {
state: {
count: 1
},
mutations: {},
actions: {}
}
search.js
export default {
state: {
count: 2
},
mutations: {},
actions: {}
}
模块要在index.js
文件也就是要在vuex modules
中注册
import Vue from 'vue'
import Vuex from 'vuex'
import A from './a'
import B from './b'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 3
},
mutations: {
},
getters: {},
actions: {
},
modules: {
A,
B
}
})
命名空间
在怎么取相关模块的值之前,先说一个命名空间
先引用官方原话
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
也就是说,你在home.js
和search.js
里面写的action
、mutation
和getter
,它自动被放到了index.js
文件下的action
、mutation
和getter
里面,解决这一问题,我们可以按照如下方式做,话不多说上代码
home.js
export default {
// 加一个 namespaced: true属性
namespaced: true,
state: {
count: 1
},
getters: {
gettersCount (state) {
return ++state.count
}
},
mutations: {},
actions: {}
}
A组件代码(注意看注释)
先上A组件代码
<template>
<div class="a-container">
<h3>我是A组件</h3>
<h4>我是A模块里面的gettersCount值:</h4>
{{ gettersCount }}
<br />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'A',
data () {
return {}
},
props: {},
components: {},
computed: {
// ...mapState({
// count: (state) => state.count
// })
// 第一个参数是模块名,第二个是你要映射的模块getters变量,你也可以用对象时映射,如果你觉得变量名称需要改变
...mapGetters('A', ['gettersCount'])
},
watch: {},
created () {
console.log(this.$store)
},
mounted () {}
}
</script>
<style scoped lang="less"></style>
上视图
这样就拿到A模块里面的getters了。action和mutation同理,在methods里面映射一下就行了
methods: {
...mapMutations('A', ['mutationsTest']),
...mapActions('A', ['actionTest'])
}
提示
别忘记加上namespaced: true
这一条属性哦~
待补充。。。。。。。