Vuex 详解

  1. 简介
  2. Vuex核心内容
  3. Vuex工作流程
  4. 用法
    1. 安装
    2. 代码组织
    3. state & mapState
    4. getters & mapGetters
    5. actions & mutations
    6. $store.dispatch & mapActions
    7. $store.commit & mapMutations
    8. modules

简介

在大型的项目中,状态往往不只是父子之间传递,而是分布于各个组件中,我们通过属性传值的方式就会显得十分臃肿,并且在Vue中,兄弟组件之间传值并没有什么好的办法,往往通过事件总线订阅发布的方式,这种方式可行,但是我们把一些共享的状态抽离出来,统一的放在一个仓库中,哪个组件需要使用,就去仓库里拿,这种方式不是更好吗,这就是状态机的作用,在React中有ReduxMobx等一些库,而在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中使用这个storestore文件夹组织类似于

storeindex.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,然后再由actioncommit一个mutation,实现状态修改

所以actionmutation之间是强耦合的,我们为了去耦合,会引入一个mutation-type.js,这个也可以帮助我们快速找到相关代码的位置。

mutation-type.js

export const CHANGE_AGE = 'change_age'
export const CHANGE_NAME = 'change_name'

然后我们定义actionmutation的时候,通过这个中间变量,进行关联;

actions & mutations

  • actions
    action整体是一个对象,key是将来我们dispatch``actiontype值,因此我们可以直接使用mutation-types导出的值,和mutaion保持一致。

    action 函数第一个参数是一个与 store 实例具有相同方法和属性的context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过context.statecontext.getters 来获取 stategetters,甚至可以通过context.dispatch派发另外一个action,可以通过解构来简化代码

    action里是可以执行异步代码的,等异步执行完了再去commit mutation,此时才是同步修改state

    actions.js

    import { 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.js

    import { 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可以直接派发一个actiondispatch接收一个对象作为参数,对象必须指定一个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可以直接提交一个mutationcommit接收一个对象作为参数,对象必须指定一个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
})

如果仅仅是区分了模块,而没有设置命名空间,那么gettersactionsmutations将会和根状态合并,外部直接获取,不需要加上命名空间

如果设置命名空间为true,则在外面获取时需要加上命名空间,如下

如果使用命名空间,那么使用gettersactionsmutaions都需要使用命名空间的写法,

[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