Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

在 vuex 中提供了几个辅助函数来帮助我们减少代码的重复和冗余。

PS: 文章辅助函数介绍部分内容引用 Vuex 文档

辅助函数介绍

mapState

由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

  1. // 创建一个 Counter 组件
  2. const Counter = {
  3. template: `<div>{{ count }}</div>`,
  4. computed: {
  5. count() {
  6. return store.state.count
  7. }
  8. }
  9. }

当一个组件需要获取多个状态时候,我们可以使用 mapState 辅助函数帮助我们生成计算属性:

  1. import { mapState } from 'vuex'
  2. export default {
  3. // ...
  4. computed: mapState({
  5. // 箭头函数可使代码更简练
  6. count: state => state.count,
  7. // 传字符串参数 'count' 等同于 `state => state.count`
  8. countAlias: 'count',
  9. // 为了能够使用 `this` 获取局部状态,必须使用常规函数
  10. countPlusLocalState(state) {
  11. return state.count + this.localCount
  12. }
  13. })
  14. }

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。如果当前 computed 中有其他的 computed,可以通过对象展开运算符 ... 与当前 vue 实例的 computed 进行合并。

  1. computed: {
  2. localComputed () { /* ... */ },
  3. // 使用对象展开运算符将此对象混入到外部对象中
  4. ...mapState({
  5. // 映射 this.count 为 store.state.count
  6. 'count'
  7. })
  8. }

mapGetters

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

  1. import { mapGetters } from 'vuex'
  2. export default {
  3. // ...
  4. computed: {
  5. // 使用对象展开运算符将 getter 混入 computed 对象中
  6. ...mapGetters([
  7. 'doneTodosCount',
  8. 'anotherGetter'
  9. // ...
  10. ])
  11. }
  12. }

如果你想将一个 getter 属性另取一个名字,使用对象形式:

  1. mapGetters({
  2. // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  3. doneCount: 'doneTodosCount'
  4. })

mapMutations

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

  1. import { mapMutations } from 'vuex'
  2. export default {
  3. // ...
  4. methods: {
  5. ...mapMutations([
  6. 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
  7. // `mapMutations` 也支持载荷:
  8. 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
  9. ]),
  10. ...mapMutations({
  11. add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
  12. })
  13. }
  14. }

mapActions

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

  1. import { mapActions } from 'vuex'
  2. export default {
  3. // ...
  4. methods: {
  5. ...mapActions([
  6. 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
  7. // `mapActions` 也支持载荷:
  8. 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
  9. ]),
  10. ...mapActions({
  11. add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
  12. })
  13. }
  14. }

createNamespacedHelpers

你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

  1. import { createNamespacedHelpers } from 'vuex'
  2. const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
  3. export default {
  4. computed: {
  5. // 在 `some/nested/module` 中查找
  6. ...mapState({
  7. a: state => state.a,
  8. b: state => state.b
  9. })
  10. },
  11. methods: {
  12. // 在 `some/nested/module` 中查找
  13. ...mapActions(['foo', 'bar'])
  14. }
  15. }

项目实战

在构建大型应用的时候,我们会将 Vuex 相关代码分割到模块中。下面是项目结构示例:

  1. ├── index.html
  2. ├── main.js
  3. ├── api
  4. └── ... # 抽取出API请求
  5. ├── components
  6. ├── App.vue
  7. └── ...
  8. └── store
  9. ├── index.js # 我们组装模块并导出 store 的地方
  10. ├── actions.js # 根级别的 action
  11. ├── mutations.js # 根级别的 mutation
  12. └── modules
  13. ├── user.js # 用户信息模块
  14. └── config.js # 配置模块
  15. └── permission.js # 权限模块

vue-helps Example 🌰

为了让小伙伴们对项目更加直观,写了个小 Demo Github | vue-helps,也可以访问 Github Page | vue-helps Example 🌰

结合 minxins

我们可以将可以复用的部分提取到 mixin 中,只要引入 mixin ,就能在页面中直接调用。

html :
**
在页面调用 checkPermission 方法,如果没有权限,则返回 false,对应的 html 不渲染。

  1. <div>
  2. <p>{{permissions}}</p>
  3. <el-button
  4. type="primary"
  5. v-if="checkPermission('vuex-helpers:permissionsButton:update')"
  6. @click="updatePermissions"
  7. >更新权限</el-button
  8. >
  9. <el-button
  10. type="danger"
  11. :disabled="!checkPermission('vuex-helpers:permissionsButton:reset')"
  12. >修改权限</el-button
  13. >
  14. </div>

javascript :
**
在 javascript 中引入 checkPermissionMixin:

  1. import checkPermissionMixin from '../mixins/checkPermissionMixin'
  2. export default {
  3. mixins: [checkPermissionMixin],
  4. created() {
  5. this.updatePermissions()
  6. // this.$store.dispatch('permission/updatePermissions')
  7. }
  8. }

checkPermissionMixin :

在 checkPermissionMixin 中调用辅助工具函数,获取权限列表,并申明 checkPermission 检测权限的函数。

  1. import { mapState, mapGetters, mapActions } from 'vuex'
  2. export default {
  3. computed: mapGetters('permission', ['permissions']),
  4. // computed: mapState({
  5. // permissions: state => state.permission.permissions
  6. // }),
  7. methods: {
  8. ...mapActions('permission', ['updatePermissions']),
  9. checkPermission(permission) {
  10. return this.permissions.includes(permission)
  11. }
  12. }
  13. }

源码解析

在 vue 的入口文件 index.js 使用 export default 默认导出了 mapState、mapMutations、 mapGetters、 mapActions、createNamespacedHelpers 辅助工具函数。

  1. import { Store, install } from './store'
  2. import {
  3. mapState,
  4. mapMutations,
  5. mapGetters,
  6. mapActions,
  7. createNamespacedHelpers
  8. } from './helpers'
  9. export default {
  10. Store,
  11. install,
  12. version: '__VERSION__',
  13. mapState,
  14. mapMutations,
  15. mapGetters,
  16. mapActions,
  17. createNamespacedHelpers
  18. }

我们可以通过解构的方式获得 vuex 暴露的辅助工具函数。

  1. import { mapState, mapMutations, mapGetters, mapActions } from 'vuex'

关于辅助工具函数的代码在 src/helpers.js:

  1. export const mapState = normalizeNamespace((namespace, states) => {
  2. ...
  3. return res
  4. })
  5. export const mapMutations = normalizeNamespace((namespace, mutations) => {
  6. ...
  7. return res
  8. })
  9. export const mapGetters = normalizeNamespace((namespace, getters) => {
  10. ...
  11. return res
  12. })
  13. export const mapActions = normalizeNamespace((namespace, actions) => {
  14. ...
  15. return res
  16. })
  17. export const createNamespacedHelpers = (namespace) => ({
  18. ...
  19. })

可以看到 helpers.js 向外暴露了 5 个辅助工具函数,在 vuex 入口文件中包装成对象后暴露出去。

mapState

mapState 辅助函数帮助我们生成计算属性。

来看一下具体实现:

  1. /**
  2. * Reduce the code which written in Vue.js for getting the state.
  3. * @param {String} [namespace] - Module's namespace
  4. * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
  5. * @param {Object}
  6. */
  7. export const mapState = normalizeNamespace((namespace, states) => {
  8. const res = {}
  9. normalizeMap(states).forEach(({ key, val }) => {
  10. res[key] = function mappedState() {
  11. let state = this.$store.state
  12. let getters = this.$store.getters
  13. if (namespace) {
  14. const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  15. if (!module) {
  16. return
  17. }
  18. state = module.context.state
  19. getters = module.context.getters
  20. }
  21. return typeof val === 'function'
  22. ? val.call(this, state, getters)
  23. : state[val]
  24. }
  25. // mark vuex getter for devtools
  26. res[key].vuex = true
  27. })
  28. return res
  29. })

mapState 函数是经过 normalizeNamespace 函数处理后返回的函数,在调用 normalizeNamespace 的时候传入了回调函数。

normalizeNamespace

我们先来看看 normalizeNamespace 函数:

  1. /**
  2. * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
  3. * @param {Function} fn
  4. * @return {Function}
  5. */
  6. function normalizeNamespace(fn) {
  7. return (namespace, map) => {
  8. if (typeof namespace !== 'string') {
  9. map = namespace
  10. namespace = ''
  11. } else if (namespace.charAt(namespace.length - 1) !== '/') {
  12. namespace += '/'
  13. }
  14. return fn(namespace, map)
  15. }
  16. }

normalizeNamespace 函数接收一个 fn 回调作为参数,也就是 mapState 传入的回调函数。

  1. ;(namespace, map) => {
  2. if (typeof namespace !== 'string') {
  3. map = namespace
  4. namespace = ''
  5. } else if (namespace.charAt(namespace.length - 1) !== '/') {
  6. namespace += '/'
  7. }
  8. return fn(namespace, map)
  9. }

此时 mapState 就是这个返回的函数,它接收 namespace 、map 作为参数,namespace 就是命名空间,map 就是传过来的 state。

首先会判断 namespace 是否是一个字符串,因为 mapState 第一个参数是可选的,如果不是字符串就说明没有命名空间,第一个参数就是传入的 state,将 namespace 赋值给 map,然后将 namespace 赋值为空字符串。进入 else if 后判断 namespace 最后一个字符串是否是 /,没有就拼上 /

当调用 mapState 的时候,就会返回 fn(namespace, map) 函数的运行后的结果,就是一个 res 对象。

PS: normalizeNamespace 是一个高阶函数实现,高阶函数是接收一个或者多个函数作为参数,并返回一个新函数的函数。
**
我们来看一下 mapState 中的 fn 具体实现。

首先申明一个 res 对象,作为循环赋值后返回结果,然后调用 normalizeMap 函数, normalizeMap 接收一个对象或者数组,转化成一个数组形式,数组元素是包含 key 和 value 的对象。

normalizeMap

  1. /**
  2. * Normalize the map
  3. * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
  4. * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
  5. * @param {Array|Object} map
  6. * @return {Object}
  7. */
  8. function normalizeMap(map) {
  9. return Array.isArray(map)
  10. ? map.map(key => ({ key, val: key }))
  11. : Object.keys(map).map(key => ({ key, val: map[key] }))
  12. }

经过 normalizeMap 函数处理后,会转化成一个数组, [{key: key, val: fn}] 的格式,调用 forEach 循环处。

在 forEach 的回调函数中,使用解构取出 key 和 value,每一次循环就以 key 为键,mappedState 函数为 value 存入 res 对象,

在 mappedState 函数中,声明 state 和 getters 变量保存 this.$store.statethis.$store.getters

接着判断传入的 namespace,如果有 namespace 就调用 getModuleByNamespace 函数搜索对应模块,如果没有搜索到就 return,有对应模块的话将对应模块的 state、getters 赋值给声明的 state 和 getters 变量。

mappedState 最后判断 val 是否是 function,是就调用 call 将 val 的 this 绑定到 Vue 实例,并将 state、 getters 作为参数传递,执行后返回,不是 function 根据 key 返回对应的 state。

getModuleByNamespace

getModuleByNamespace 函数主要用来搜索具有命名空间的模块。

  1. /**
  2. * Search a special module from store by namespace. if module not exist, print error message.
  3. * @param {Object} store
  4. * @param {String} helper
  5. * @param {String} namespace
  6. * @return {Object}
  7. */
  8. function getModuleByNamespace(store, helper, namespace) {
  9. const module = store._modulesNamespaceMap[namespace]
  10. if (process.env.NODE_ENV !== 'production' && !module) {
  11. console.error(
  12. `[vuex] module namespace not found in ${helper}(): ${namespace}`
  13. )
  14. }
  15. return module
  16. }

函数开始申明 module 变量,然后根据 namespace 从 store._modulesNamespaceMap 取出对应模块,
_modulesNamespaceMap 这个变量是在 Store 类中,调用 installModule 时候保存所以所有命名空间模块的变量。

判断非生产环境并且没有对应模块,抛出异常,最后将 module 变量返回。

forEach 最后还有一段:

  1. // mark vuex getter for devtools
  2. res[key].vuex = true

应该是 devtools 需要这个属性判断 value 是否属于 vuex。

完成 forEach 循环后会将处理后的 res 对象返回。

mapMutations

mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用。

来看一下具体实现:

  1. /**
  2. * Reduce the code which written in Vue.js for committing the mutation
  3. * @param {String} [namespace] - Module's namespace
  4. * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
  5. * @return {Object}
  6. */
  7. export const mapMutations = normalizeNamespace((namespace, mutations) => {
  8. const res = {}
  9. normalizeMap(mutations).forEach(({ key, val }) => {
  10. res[key] = function mappedMutation(...args) {
  11. // Get the commit method from store
  12. let commit = this.$store.commit
  13. if (namespace) {
  14. const module = getModuleByNamespace(
  15. this.$store,
  16. 'mapMutations',
  17. namespace
  18. )
  19. if (!module) {
  20. return
  21. }
  22. commit = module.context.commit
  23. }
  24. return typeof val === 'function'
  25. ? val.apply(this, [commit].concat(args))
  26. : commit.apply(this.$store, [val].concat(args))
  27. }
  28. })
  29. return res
  30. })

mapMutations 处理过程与 mapState 相似,我看来看看传入 normalizeNamespace 的回调函数。

首先也是申明 res 空对象,经过 normalizeMap 函数处理后的 mutations 调用 forEach 循环处理,在 forEach 的回调函数中, 使用解构取出 key 和 value,每一次循环就以 key 为键、mappedMutation 函数为 value 存入 res 对象, 在 mappedMutation 函数中,声明 commit 变量保存 this.$store.commit 。

判断传入的 namespace,如果有 namespace 就调用 getModuleByNamespace 函数搜索对应模块,如果没有搜索到就 return,有对应模块的话对应模块的将 commit 赋值给声明的 commit 变量。

mappedMutation 最后判断 val 是否是 function,是就调用 apply 将 val 的 this 绑定到 Vue 实例,并将 commit 和 args 合并成一个数组作为参数传递,,val 不是 function 就将 commit 调用 apply 改变了 this 指向,将 val 和 args 合并成一个数组作为参数传递,执行后返回。

最后将 res 对象返回。

mapGetters

mapGetters 辅助函数将 store 中的 getter 映射到局部计算属性。

来看一下具体实现:

  1. /**
  2. * Reduce the code which written in Vue.js for getting the getters
  3. * @param {String} [namespace] - Module's namespace
  4. * @param {Object|Array} getters
  5. * @return {Object}
  6. */
  7. export const mapGetters = normalizeNamespace((namespace, getters) => {
  8. const res = {}
  9. normalizeMap(getters).forEach(({ key, val }) => {
  10. // thie namespace has been mutate by normalizeNamespace
  11. val = namespace + val
  12. res[key] = function mappedGetter() {
  13. if (
  14. namespace &&
  15. !getModuleByNamespace(this.$store, 'mapGetters', namespace)
  16. ) {
  17. return
  18. }
  19. if (
  20. process.env.NODE_ENV !== 'production' &&
  21. !(val in this.$store.getters)
  22. ) {
  23. console.error(`[vuex] unknown getter: ${val}`)
  24. return
  25. }
  26. return this.$store.getters[val]
  27. }
  28. // mark vuex getter for devtools
  29. res[key].vuex = true
  30. })
  31. return res
  32. })

我看来看看传入 normalizeNamespace 的回调函数。

首先也是申明 res 空对象,经过 normalizeMap 函数处理后的 getters 调用 forEach 循环处理,在 forEach 的回调函数中, 使用解构取出 key 和 value,每一次循环就以 key 为键、mappedGetter 函数为 value 存入 res 对象,这里会将 val 赋值成 namespace + val,如果有命名空间,此时的 val 应该是类似这样的: cart/cartProducts

在 mappedGetter 函数中,首先判断如果有 namespace 并且调用 getModuleByNamespace 函数没有匹配到对应模块就直接 return。

然后判断在非生产环境并且 this.$store.getters 没有对应的 val 就抛出异常并返回。接下来就是有对应模块的情况,直接返回 this.$store.getters 对应的 getter。

最后将 res 对象返回。

mapActions

mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用。

来看一下具体实现:

  1. /**
  2. * Reduce the code which written in Vue.js for dispatch the action
  3. * @param {String} [namespace] - Module's namespace
  4. * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.
  5. * @return {Object}
  6. */
  7. export const mapActions = normalizeNamespace((namespace, actions) => {
  8. const res = {}
  9. normalizeMap(actions).forEach(({ key, val }) => {
  10. res[key] = function mappedAction(...args) {
  11. // get dispatch function from store
  12. let dispatch = this.$store.dispatch
  13. if (namespace) {
  14. const module = getModuleByNamespace(
  15. this.$store,
  16. 'mapActions',
  17. namespace
  18. )
  19. if (!module) {
  20. return
  21. }
  22. dispatch = module.context.dispatch
  23. }
  24. return typeof val === 'function'
  25. ? val.apply(this, [dispatch].concat(args))
  26. : dispatch.apply(this.$store, [val].concat(args))
  27. }
  28. })
  29. return res
  30. })

mapActions 处理过程与 mapMutations 函数一模一样,就不在赘述。

createNamespacedHelpers

createNamespacedHelpers 创建基于某个命名空间辅助函数。

来看一下具体实现:

  1. /**
  2. * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
  3. * @param {String} namespace
  4. * @return {Object}
  5. */
  6. export const createNamespacedHelpers = namespace => ({
  7. mapState: mapState.bind(null, namespace),
  8. mapGetters: mapGetters.bind(null, namespace),
  9. mapMutations: mapMutations.bind(null, namespace),
  10. mapActions: mapActions.bind(null, namespace)
  11. })

createNamespacedHelpers 函数接受一个字符串作为参数,返回一个包含 mapState 、mapGetters 、mapActions 和 mapMutations 的对象。

以 mapState 为例,调用 mapState 函数的 bind 方法,将 null 作为第一个参数传入,不会改变 this 指向,namespace 作为第二个参数。

  1. import { createNamespacedHelpers } from 'vuex'
  2. const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
  3. export default {
  4. computed: {
  5. // 在 `some/nested/module` 中查找
  6. ...mapState({
  7. a: state => state.a,
  8. b: state => state.b
  9. })
  10. },
  11. methods: {
  12. // 在 `some/nested/module` 中查找
  13. ...mapActions(['foo', 'bar'])
  14. }
  15. }

此时的 mapState 函数就是经过 bind 处理过的,会将 namespace 作为第一个参数传入。

相当于下面这样:

  1. ...mapState('some/nested/module', {
  2. a: state => state.a,
  3. b: state => state.b
  4. })

简化了重复写入命名空间。

到此 helpers.js 结束。