紧接上一篇路由
还是来吧废话少说,我们争取一篇文章就搞定vuex的使用及原理(用法主要提炼的官网)
最近一直在受虐,状态不是很好。如有误,望指教,感恩!
2.1 先说一下概念
vuex比起vue-router来说知识点便要少的多了。
这里只要搞明白5个东西就可以了,分别是State,Getter,Mutation,Action,Module
2.2 重点
State
基本使用
直接上栗子,向state中放一个数据
state: {
count: 0,
},
怎么在组件中获取呢?和vue-router类似,在根vue实例的时候是把这个状态实例传进了vue的构造函数中,组件实例可以使用this.$store拿到这些状态
组件中获取
<template>
<div id="app">
{{$store.state.count}}
</div>
</template>
值得注意的是:我们直接操作数据,但是为了保证状态的可监控这种操作是不被允许的,在vuex中操作数据的唯一方式就是触发一个mutation
为了不要每次拿数据都使用$store.state.count这种方式去拿,我们可以把它放进我们的计算属性中
computed:{
count(){
return this.$store.state.count
},
}
但是,如果当前组件要使用state中的多个数据,我们难道要在计算属性中一个个声明嘛,显然太麻烦了
辅助函数 mapState
vuex为我们提供了一个辅助函数 mapState
利用这个辅助函数帮我们声明计算属性
栗子
store中
state: {
count: 0,
name: 'gxb',
age: 18
},
组件中
import {mapState} from 'vuex'
export default {
computed:mapState(
['count','name','age']
)
};
现在就好使多了
<template>
<div id="app">
{{count}}{{name}}{{age}}
</div>
</template>
Getter
基本使用
Getter就是相当于vuex的一个计算属性,由state中的数据派生而来,原数据改变这个派生数据才会跟着改变,否则就使用缓存
直接上栗子
每个getter接收一个state作为参数
export default new Vuex.Store({
state: {
count: 0,
name: 'gxb',
age: 18
},
getters: {
demo: state => {
return `${state.name} is ${state.age} 岁了`
}
}
}
组件中获取
<template>
<div id="app">
{{$store.getters.demo}}
</div>
</template>
getter也是可以接收一个别的getter作为它的第二个参数的
getters: {
demo: (state, getters) => {
return `${state.name} is ${state.age} 岁了` + getters.demo02
},
demo02: state => {
return state.count
}
}
一个getter的返回值也可以是个函数
getters: {
demo: (state, getters) => {
return `${state.name} is ${state.age} 岁了` + getters.demo02
},
demo02: state => {
return state.count
},
demo03: state => n => state.count === n
}
组件
<template>
<div id="app">
{{$store.getters.demo03(0)}}
</div>
</template>
开始当然是true了
在组件中为了不一直使用this.$store.getters这种方式拿数据,也可以把它放到当前组件的计算属性中
computed:{
demo(){
return this.$store.getters.demo
}
}
辅助函数mapGetters
当然如果是多个getter,vuex也提供了一个辅助函数
import { mapGetters } from 'vuex'
export default {
computed:{
//...原有计算属性
...mapGetters(['demo'])
},
Mutation
基本使用
修改state中数据的唯一方式就是触发一个Mutation。Mutation类似一个事件注册,触发一个Mutation需要使用commit方法
直接看栗子
store中(每个mutation的第一参也是state)
state: {
count: 0,
name: 'gxb',
age: 18
},
mutations: {
add(state) {
state.count++
},
},
组件中
<button @click="$store.commit('add')">add</button>
commit第一参就是要触发的mutations的事件类型(即add就相当于一个mutations的事件类型,后面的函数体就相当于事件的回调函数)
当然commit是可以传入第二个参数的
再来个栗子
store中(mutation当然也需要第二个参数来收一下吧)
mutations: {
add(state) {
state.count++
},
add02(state, n) {
state.count += n
}
},
组件中
指定加法步长
<button @click="$store.commit('add02',2)">add</button>
这个commit的第二参还有一个名称叫做 载荷,这个 载荷 一般情况下多为一个对象(对象传的东西多啊)
<button @click="$store.commit('add02',{
n:2
})">add</button>
mutations: {
add(state) {
state.count++
},
add02(state, obj) {
state.count += obj.n
}
},
commit的参数可以是一个对象
this.$store.commit({
type:'add02',
n:2
})
这个时候整个对象都是一个 载荷
mutations: {
increment (state, payload) {
state.count += payload.n
}
}
还是老问题,我们一直使用$store.commit这样太难受了吧
辅助函数mapMutations
vuex又给我们提供了一个辅助函数mapMutations
直接看栗子吧
import { mapState } from "vuex";
import { mapGetters } from "vuex";
import { mapMutations } from "vuex";
export default {
computed: {
...mapState(["count"]),
...mapGetters(["demo"]),
},
methods: {
...mapMutations(["add"]),
},
};
这个时候,methods对象就相当于有了一个add函数,调用这个add函数就等价于调用$store.commit('add')
<button @click="this.add">add</button>
这时舒服多了吧
Mutation这个需要注意几个问题
- Mutation要遵循vue的响应式规则(因为vuex中所有的状态数据都应该是响应式的,不能明知道vue不能监听一个新增属性你还往state中新添一个吧。可借助Vue.set或者使用老对象作为容器)
- Mutation里必须都是同步函数(无法确定回调函数的执行时机,devtools无法记录)
Action
基本使用
既然Mutation无法进行异步操作,那么异步就交个Action了,Action也不能直接操作state中的数据,它也是通过触发Mutation来间接改变的
commit用于触发mutation,dispatch用来触发action
栗子
store中:每一个action的第一参是一个与store实例具有相同方法属性的上下文对象,故可以从中解构出commit方法
state: {
count: 0,
name: 'gxb',
age: 18
},
mutations: {
add(state) {
state.count++
},
add02(state, n) {
state.count += n
}
},
actions: {
test({ commit }) {
commit('add')
}
},
组件中
<button @click="$store.dispatch('test')">触发actions</button>
一般在action中执行异步操作
state: {
count: 0,
name: 'gxb',
age: 18
},
mutations: {
add(state) {
state.count++
},
add02(state, n) {
state.count += n
}
},
actions: {
increment({ commit }) {
setTimeout(function() {
commit('add')
}, 1000)
}
},
Actions 支持同样的载荷方式和对象方式进行分发
state: {
count: 0,
name: 'gxb',
age: 18
},
mutations: {
add(state) {
state.count++
},
add02(state, obj) {
state.count += obj.n
}
},
actions: {
increment({ commit }, obj) {
setTimeout(function() {
commit('add02', obj)
}, 1000)
}
},
<button @click="$store.dispatch('increment',{
n:2
})">触发actions</button>
以对象方式触发一个action
<button @click="$store.dispatch(
{
type:'increment',
n:2
})">触发actions</button>
组合Action
组合Action,因为一般Action是处理异步的操作。如果有多个Action,比较有Action01,和Action02,有个要求就是需要Action01先处理一个数据,完成之后Action02在进行处理。我们得怎样组合它呢
首先来看store.dispatch这个方法的返回值是个啥东西
它是一个promise
那么栗子就来咯,先让action01---处理,完成之后再交给action02处理。
actions: {
action01({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('add')
resolve('s')
}, 1000)
reject('f')
})
},
action02({ commit, dispatch }) {
dispatch('action01').then(() => {
commit('add')
})
}
},
或者使用async,await
官网的栗子
假设两个异步取数据的方法,返回值是promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
辅助函数
辅助函数与mutation一样,就省略了
Module
为啥出去这个,因为vuex是使用的单一状态树,也就是说所有的状态,以及状态派生的getter,改变状态的mutation还有操作异步的actions都在一个文件中。
如果是一个复杂应用,我去,想想就有优点恐怖了
简单使用
为了解决这个问题,vuex可以将store分割成一个个的模块,每个模块都有那一套东西(state、mutations、actions、getters)
像这样(为了方便演示写一块了)
const module01 = {
state: () => ({
count: 1
}),
mutations: {
add(state) {
state.count++
}
},
actions: {
addAsync({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
getters: {
countToStr02(state) {
return state.count + ''
}
}
}
const module02 = {
state: () => ({
count: 2
}),
mutations: {
add(state) {
state.count++
}
},
actions: {
addAsync({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
getters: {
countToStr03(state) {
return state.count + ''
}
}
}
export default new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
state.count++
}
},
actions: {
addAsync({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
getters: {
countToStr01(state) {
return state.count + ''
}
},
modules: {
module01,
module02
},
})
在组件中咋拿module01的状态呢?
像这样
{{$store.state.module01}}
怎么改变状态呢?
你仍然还是要触发的mutations,现在先可以把他们理解成全局的
就使用store.commit('add')去改变就行,不过此时你这样会触发两个mutation,一个mutation是用来改变根模块状态的,一个是改变模块module01中的状态的
栗子
<template>
<div id="app">
{{$store.state.count}}
{{$store.state.module01}}
<button @click="$store.commit('add')">测试</button>
</div>
</template>
模块的局部状态
上面的栗子其实已经用了一部分了
对于模块中的mutations和getters,它们的第一参仍是当前此模块的状态对象,只不过getter多了一个第三参,是根模块状态对象;action也是和原来一样,只不过此上下文对象中又多了一个可以拿根模块状态的对象即context.rootState
命名空间
以上的模块都是注册在全局的,即像上面一个commit触发了两个模块的mutation
严格来说,我们肯定是不希望这种多变化的情况,我们一般还是喜欢比较受控的玩意
操作也很简单,就在模块里加一个属性namespaced值设置为true
那么我们再次触发那个add的mutation时就可以这样写了
<button @click="$store.commit('module01/add')">测试</button>
getters和dispatch和commit一样,这是就只触发module01模块的add类型的mutation了
嵌套的模块会继承父模块的命名空间
还有一些小知识点
1 如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
2 若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
单解释第二句话吧,你在有命名空间的模块的下,比如你想触发一个mutation,你在action中得这样写吧commit(...),这里是默认给你加上当前命名空间的路径的,如果你此时想要触发的是一个处于跟模块的mutation,你就可以加上{ root: true }这个参数
即commit('demo',null,{ root: true })(假设没有载荷)
3 若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中 (吃不了撑的不在根模块注册?唉)
瞅一样官网的栗子
actions: {
someAction: {
root: true,
handler (context, payload) { ... } // -> 'someAction'
}
}
4 模块的动态创建
store创建之后
可以使用store.registerModule('myModule',{...})创建模块
store.registerModule(['nested', 'myModule'], {})创建嵌套的nested/myModule
5 模块重用
state不用对象了,和vue的data一样使用函数返回一个新对象装数据
2.3 实现一个简单版的vuex
有了上一个router的栗子,这个就更加简单了吧
还是先来简单分析一下
- 首先是一个插件故要有个install方法
- 向内传了一个对象{state、getters....},且在实例store中可以拿。故这些东西是放进了构造函数中的
- 有两个api,commit和dispatch
接下来就简单多了
首先还是和router一样,将store使用混入放进vue原型中
先实现install方法吧
let Vue
function install(vue) {
Vue = vue//还是为了保证下面可以拿到vue
vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
初始化Store类,简单做一下getter的代理
这里仅是对getter做了一下代理(本来我打算getter的计算属性功能借助一下vue的计算属性呢,结果发现如果那样的话getter的处理函数是在vue中调用的,我无法进行传递实参this.state。也不想在这搞的太复杂,就先这样吧等我找到一个比较好的解决方法在改进一下)
class Store {
constructor(options = {}) {
this._vm = new Vue({
data: {
state: options.state
},
})
this.actions = options.actions || {}
this.mutations = options.mutations || {}
this.getters = {}
let getters = options.getters || {}
// 就是简单做了一次代理
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return getters[key](this.state)
}
})
})
}
//state的存储器
get state() {
return this._vm.state
}
}
实现commit
这个都比较简单,就是根据传过来的mutation类型,去拿处理函数并进行调用
commit(type, payload) {
const entry = this.mutations[type]
if (!entry) {
throw new Error('没有此mutation')
} else {
entry(this.state, payload)
}
}
实现dispatch
与上面基本是一样,不过action的第一参是与store实例具有相同属性方法的上下文对象.因为这里没有打算继续实现模块化,故我们就将把当前实例给他
同时不要忘了dispatch返回的是一个promise
dispatch(type, payload) {
const entry = this.actions[type]
if (!entry) {
throw new Error('没有此action')
}
// 返回的是一个promise
return Promise.resolve(entry(this, payload))
}
这里还有一个小问题。
如我们的store配置文件,写一个action时可能是这样写的
actions: {
addAsync({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
我们都之前这种定时器中的回调函数this都是指的undefined(严格模式),但是commit函数中是需要通过this.state拿状态数据的,此时this指向undefined而不是store实例。故数据无法获取
改进:提前将commit内部中的this绑定好(dispatch也一样)
constructor(options = {}) {
//...
this.commit = this.commit.bind(this)
}
完整代码
let Vue
function install(vue) {
Vue = vue
vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
class Store {
constructor(options = {}) {
this._vm = new Vue({
data: {
state: options.state
},
created() {
window.state = this.state
},
computed: {
},
})
this.actions = options.actions || {}
this.mutations = options.mutations || {}
this.getters = {}
let getters = options.getters || {}
// 就是简单做了一次代理
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => {
return getters[key](this.state)
}
})
})
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state() {
return this._vm.state
}
commit(type, payload) {
const entry = this.mutations[type]
if (!entry) {
throw new Error('没有此mutation')
} else {
entry(this.state, payload)
}
}
dispatch(type, payload) {
const entry = this.actions[type]
if (!entry) {
throw new Error('没有此action')
}
// 返回的是一个promise
return Promise.resolve(entry(this, payload))
}
}
export default { install, Store }
写到最后
希望这篇文章能给与您一丝帮助(卑微求赞)
期待着我们的下一次邂逅