简介
在大型的项目中,状态往往不只是父子之间传递,而是分布于各个组件中,我们通过属性传值的方式就会显得十分臃肿,并且在Vue中,兄弟组件之间传值并没有什么好的办法,往往通过事件总线订阅发布的方式,这种方式可行,但是我们把一些共享的状态抽离出来,统一的放在一个仓库中,哪个组件需要使用,就去仓库里拿,这种方式不是更好吗,这就是状态机的作用,在React中有Redux,Mobx等一些库,而在Vue中,官方推出的Vuex是最适合Vue的,Vuex中的状态是响应式的,完美的结合了Vue的优势;
Vuex核心内容
在Vuex对象中,其实不止有state,还有用来操作state中数据的方法集,以及当我们需要对state中的数据需要加工的方法集等等成员。
成员列表:
state存放状态getters加工state成员给外界mutations同步state成员操作actions异步操作modules模块化状态管理
Vuex工作流程
官网给出的流程图
首先,Vue组件如果调用某个Vuex的方法过程中需要向后端请求时或者说出现异步操作时,需要dispatch Actions的方法,以保证数据的同步。可以说,action的存在就是为了让mutations中的方法能在异步操作中起作用。
如果没有异步操作,那么我们就可以直接在组件内提交状态中的Mutations中自己编写的方法来达成对state成员的操作。但是不建议这样做,我们希望总是由action去调用mutations修改state
不建议在组件中直接对state中的成员进行操作,这是因为直接修改(例如:this.$store.state.name = 'hello')的话不能被VueDevtools所监控到。同样,总是应该有mustaions去修改state
最后被修改后的state成员会被渲染到组件的当中去。
这样就形成了一个单向闭环,这样的好处是可以明确的知道数据的流向,避免数据混乱;
用法
安装
npm install vuex --save
或者
yarn add vuex
代码组织
一般我们把全局状态单独放在一个文件夹中,然后把store导出,在main.js中使用这个store,store文件夹组织类似于
store的index.js文件类似于下面代码,由于Vuex实例化之前需要Vue.use()一下,因此需要先导入vue
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import state from './state'
import mutations from './mutations'
import getters from './getters'
import actions from './actions'
// 单独模块
import users from './modules/users'
export default new Vuex.Store({
state,
mutations,
actions,
getters,
modules: {
users
}
})
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store,
render: h => h(App)
}).$mount('#app')
state & mapState
state的声明很简单,就是一个对象,里面存放的是响应式数据
state.js
export default {
name: 'jerry',
age: 18
}
由于我们把state抽离出来了,因此直接导出对象就行了
数据声明了,怎么使用呢? 在组件中我们直接可以拿到store里的state,有以下几种方式
直接在模板中使用
<template> <div> {{ $store.state.name }} {{ $store.state.age }} </div> </template>computed中指定当不使用
mapState辅助函数的时候,直接定义计算属性,使用this.$store.state获取状态,如果很多状态,这样写代码十分冗余但是如果需要拿到状态和本地数据进行结合,这样写是很方便的,(无法使用过滤器,过滤器中无法访问实例this)
<template> <div> {{ name }} {{ age }} </div> </template> <script> import { mapState } from 'vuex' export default { // ... computed: { name() { return this.$store.state.name }, age() { return this.$store.state.age + this.localData } } } </script>mapState辅助函数mapState的第一个参数是命名空间,字符串,结合modules使用,可省略,第二个参数可以是一个数组或者对象<template> <div> {{ name }} {{ age }} </div> </template> <script> import { mapState } from 'vuex' export default { // ... computed: { ...mapState(['name', 'age']) } } </script>如果传入对象,必须定义一个计算属性名称,不能使用ES6的
{name, age}的省略写法,否则报错<template> <div> {{ stateName }} {{ stateAge }} </div> </template> <script> import { mapState } from 'vuex' export default { // ... computed: { ...mapState({ stateName: 'name', stateAge: 'age' }) } } </script>
getters & mapGetters
getter可认为是state的计算属性,将state作为第一个参数,可以拿到state里的数据,并且还可以接受第二个参数,就是getters,用来调用其他getter
getters.js
export default {
// getter将state作为第一个参数
ageWithSuffix(state) {
return state.age + '岁'
},
// getter也可将getters作为第二个参数,用来调用其他getter
ageWithPrefix(state, getters) {
return '今年' + getters.ageWithSuffix
}
}
如何使用getters?
模板里直接使用
<template> <div> {{ $store.getters.ageWithSuffix }} {{ $store.getters.ageWithPrefix }} </div> </template>computed里使用<template> <div> {{ prefix }} </div> </template> <script> export default { // ... computed: { prefix(){ return this.$store.getters.ageWithPrefix } } } </script>mapGetters辅助函数
数组写法<template> <div> {{ ageWithSuffix }} {{ ageWithPrefix }} </div> </template> <script> import { mapGetters } from 'vuex' export default { // ... computed: { ...mapGetters(['ageWithSuffix', 'ageWithPrefix']), } } </script>对象写法
<template> <div> {{ suffix }} {{ prefix }} </div> </template> <script> import { mapGetters } from 'vuex' export default { // ... computed: { ...mapGetters({ suffix: 'ageWithSuffix', prefix: 'ageWithPrefix' }) } } </script>
上面是定义状态以及获取状态的方法,而修改状态需要dispatch一个action,然后再由action去commit一个mutation,实现状态修改
所以action和mutation之间是强耦合的,我们为了去耦合,会引入一个mutation-type.js,这个也可以帮助我们快速找到相关代码的位置。
mutation-type.js
export const CHANGE_AGE = 'change_age'
export const CHANGE_NAME = 'change_name'
然后我们定义action和mutation的时候,通过这个中间变量,进行关联;
actions & mutations
actions
action整体是一个对象,key是将来我们dispatch``action的type值,因此我们可以直接使用mutation-types导出的值,和mutaion保持一致。action函数第一个参数是一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getters来获取state和getters,甚至可以通过context.dispatch派发另外一个action,可以通过解构来简化代码action里是可以执行异步代码的,等异步执行完了再去commitmutation,此时才是同步修改stateactions.jsimport { CHANGE_AGE, CHANGE_NAME } from './mutation-types' export default { // 第二个参数是payload,用于接收数据,通常为一个对象,用于接收多个数据,也可以直接接受单个参数 [CHANGE_NAME]({ commit }, payload) { commit(CHANGE_NAME, payload) }, }如果我们利用
async / await,我们可以如下组合action,在action中调用另外一个action// 假设 getData() 和 getOtherData() 返回的是 Promise async actionA({ commit }) { commit('gotData', await getData()) }, async actionB({ dispatch, commit }) { await dispatch('actionA') // 等待 actionA 完成 commit('gotOtherData', await getOtherData()) }mutations
mutation接收两个参数,一个是state,用于修改状态,一个是payload,用于接收action传参,payload大多数为一个对象,用以传递多个数据mutations.jsimport { CHANGE_AGE, CHANGE_NAME } from './mutation-types' export default { // [CHANGE_AGE] 为es6语法,可以将表达式作为属性名 [CHANGE_AGE](state, payload) { console.log(payload); state.age = payload.age }, [CHANGE_NAME](state, payload) { state.name = payload.name }, }
$store.dispatch & mapActions
$store.dispatch
$store.dispatch可以直接派发一个action,dispatch接收一个对象作为参数,对象必须指定一个type属性,表示派发哪一个action,或者把type放在第一个参数上<template> <div> <button @click="changeName">修改名字</button> </div> </template> <script> import { CHANGE_NAME } from '@/store/mutation-types' export default { // ... methods: { changeName() { this.$store.dispatch({ type: CHANGE_NAME, name: 'tom' }) // 把type放在第一个参数上 this.$store.dispatch(CHANGE_NAME, { name: 'tom' }) } } } </script>mapActions
mapActions辅助函数将组件的methods映射为store.dispatch调用import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` // `mapActions` 也支持载荷: 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions({ add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`,相当于取了个别名 }) } }引入
mutation-types的例子:<template> <div> <button @click="changeName">修改名字</button> </div> </template> <script> import { CHANGE_NAME } from '@/store/mutation-types' import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([CHANGE_NAME]), changeName() { this[CHANGE_NAME]({name: 'tom'}) } } } </script>
$store.commit & mapMutations
$store.commit
我们是可以直接跳过
action直接提交一个mutation的,但是不建议这么做,即使你知道并没有异步操作$store.commit可以直接提交一个mutation,commit接收一个对象作为参数,对象必须指定一个type属性,表示提交哪一个mutation,或者把type放在第一个参数上<template> <div> <button @click="changeAge">修改年龄</button> </div> </template> <script> import { CHANGE_NAME, CHANGE_AGE } from '@/store/mutation-types' export default { // ... methods: { changeAge() { this.$store.commit({ type: CHANGE_AGE, age: 20 }) // 把type放在第一个参数上 this.$store.commit(CHANGE_AGE, { age: 20 }) } } } </script>mapMutations
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')` // `mapMutations` 也支持载荷: 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')` }) } }引入
mutation-types的例子:<template> <div> <button @click="changeAge">修改年龄</button> </div> </template> <script> import { CHANGE_AGE } from '@/store/mutation-types' import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([CHANGE_AGE]), changeAge() { this[CHANGE_AGE]({age: 20}) } } } </script>
modules
在外面使用state时,通过 {{$store.state.users.username}} 获取,
这里的users是根据new Vuex.Store({ modules: { users: users } }) 的key来指定的
是否启用命名空间对模块内的state都没有影响,取值的方式都需要加上命名空间
因此取值方式变成了
<template>
{{$store.state.users.username}}
</template>
或者
...mapState('account', ['user'])
或者
...mapState({
tomRole: state => state.users.role,
tomAge: state => state.users.age
})
如果仅仅是区分了模块,而没有设置命名空间,那么getters、actions、mutations将会和根状态合并,外部直接获取,不需要加上命名空间
如果设置命名空间为true,则在外面获取时需要加上命名空间,如下
如果使用命名空间,那么使用getters、actions、mutaions都需要使用命名空间的写法,
[this.]$store.getters['account/isAdmin'] 或 ...mapGetters('account', ['isAdmin'])
[this.]$store.dispatch('account/login') 或 ...mapActions('account', ['login'])
[this.]$store.commit('account/login') 或 ...mapMutations('account', ['login'])
export default {
namespaced: true,
// 可以注意到state是一个函数返回了对象,避免自己和其他state数据互相污染,实际上和Vue组件内的data是同样的问题
state() {
return {
username: 'Tom',
role: 'admin',
age: 20
}
},
// 在这个模块的 getter 中,`getters` 被局部化了, 你可以使用 getter 的第四个参数来调用 `rootGetters`
// 如果你希望使用全局 state 和 getter,rootState 和 rootGetters 会作为第三和第四参数传入 getter,
// 也会通过 context 对象的属性传入 action。
getters: {
role(state, getters, rootState, rootGetters) {
return state.role
},
allAge(state, getters, rootState, rootGetters) {
return state.age + rootState.age
}
},
// 对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
actions: {
change_role({ commit, rootState }) {
commit('m_change_role')
},
// 在这个模块中, dispatch 和 commit 也被局部化了,他们可以接受 `root` 属性以访问根 dispatch 或 commit
// 若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
someAction({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'users/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'users/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'users/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
// 若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:
globalAction: {
root: true,
handler(namespacedContext, payload) { }
}
},
mutations: {
m_change_role(state, payload) {
state.role = 'vip'
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com