vuex 深入学习

799 阅读13分钟

概述

在开发 复杂vue应用 时,vuex 可以帮我们管理 多个组件中的共享状态

使用 new Vuex.store(options), 可以构建一个 store 实例。store 是 vuex应用 的核心,通过 store.state 可以访问应用中的 共享状态, 通过 store.getters 可以访问 共享状态派生出的新的状态,通过 store.commit 方法可以提交 mutation更改共享状态,通过 store.dispatch 方法可以 派发 action异步提交 mutaion

另外, 如果 应用变得复杂导致store变得比较臃肿 的时候, 我们可以将 store 分割成 module, 每个 module 可拥有自己的 stategettersmutationsactions 甚至 嵌套子modules

问题

  1. vuex是怎么安装的? 安装过程中做了哪些工作?

  2. 为什么修改state中的数据,会触发更新?

  3. getter的工作原理?

  4. 为什么不建议通过 store.state.xx 的方式直接修改vuex的状态?

  5. 命名空间对state、getter、mutation、action的影响

  6. 严格模式是怎么工作的?

  7. state、getter、mutation、action在组件中的使用

  8. 其他

安装 vuex

使用 vuex 开发 vue应用 的时候,需要先安装 vuex

vuex 的安装过程如下:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

vue 在通过 use 安装 vuex 的时候, 会自动执行 vuex 提供的 install方法。 vuex 提供的 install 方法如下:

// install

function install (_Vue) {
  if (Vue && _Vue === Vue) {
    {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  // 执行applyMixin方法
  applyMixin(Vue);
}

vuex 提供的 install 方法中,主要过程是执行 applyMixin 方法,具体过程如下:

// applyMixin

function applyMixin (Vue) {
  // 获取当前使用的vue的版本
  const version = Number(Vue.version.split('.')[0]);

  if (version >= 2) {
    // vue2及以上 使用,每一个vue实例构建时触发 beforeCreate 钩子函数, 执行 vuexInit 方法
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    // vue1 使用
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init;
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit;
      _init.call(this, options);
    };
  }
  
  // 每一个vue实例构建的时候, 都会执行vuexInit方法
  function vuexInit () {
    // this -> 当前vue实例
    const options = this.$options;
    // store injection
    if (options.store) {
      // 根vue实例 的 $store 属性
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store;
    } else if (options.parent && options.parent.$store) {
      // 组件vue实例, $store属性, 指向根vue实例的$store属性
      this.$store = options.parent.$store;
    }
  }

applyMixin 方法,全局注册 beforeCreate 钩子,即 每一个vue实例在构建的时候都会触发 beforeCreate 钩子,执行 vuexInit 方法

执行 vuexInit 方法的时候, 为 vue实例 创建 $store属性。不管是 根vue实例 还是 组件vue实例$store属性 都指向通过 new Vuex.store 构建的 store实例,即 所有的 vue实例 的 $store 属性值都相同

综上, vuex 安装的时候就做了 一件事:给 vue全局注册 beforeCreate。当创建 vue实例 的时候,触发 beforeCreate,给 vue实例 添加 $store 属性,指向通过 new Vuex.store 构建的 store实例

vuex - 响应式工作原理

在使用 vuex 开发 vue应用 的时候, 我们会定义 state, 然后在 组件 中通过 计算属性(computed) 使用定义好的 state。 如下:

var option = {
    state: {
        money: 10000
    },
    mutations: {
        setMoney(state, money) {
            state.money = money
        }
    }
}

var bill = {
    data() {
        return {...}
    },
    computed: {
        finalMoney() {
            // money的单位为 厘
            return parseInt(this.$store.state.money / 1000)
        }
    },
    methods: {
        setDisount() {
            this.$store.commit('setMoney', this.money)
        }
    }
    ...
    
}

在上面的 示例 中,账单组件 在计算 费用 的时候,使用了 vuex状态 - money。 当通过 组件方法 setMoney 修改 vuex状态 - money, 账单组件之前计算的费用 也会 更新

那么问题来了, vuex的响应式更新是怎么触发的?

其实,vuex的响应式原理 是基于 vue的响应式原理 实现的

构建vue实例 的时候,如果 配置项 中包含 数据属性data, 会 深度遍历data中的每一个属性,为 每一个属性 建立一个 deps列表,并通过 defineProperty 方法为 每一个属性 添加 gettersetter。如果 template模板计算属性监听属性 使用了 data属性, 会 触发data属性的getter,对应的 watcher 会添加到 data属性的deps列表 中。 当 data属性 发生变化时, 触发setter通知deps列表中的watcher更新 ,然后 重新渲染界面返回新的计算属性的值触发监听callback

vuex响应式更新, 是在使用 new Vuex.store(options) 构建 store实例 的时候 实现 的,经历的 主要流程 如下:

  1. 建立 根module 以及 子modules

    在构建 store实例 的时候,首先会根据传入的 配置项 options(state、getters、mutation等) 生成一个 root module对象。如果 options 中包含 modules以及嵌套modules, 那么会遍历 modules 及嵌套 modules, 建立对应的 module 对象

    每一个 module 对象 都包含各自的 stategettersactonsmutations 以及 嵌套子modules

    module 对象 会通过 children属性(array), 收集 关联的 子module 对象

  2. 嵌套子modules 中的 state 收集到 根modulestate 中。

    // options
    var option = {
        state: {
            name: ''
        },
        modules: {
            trade: {
                state: {
                    money: 1000
                },
                modules: {
                    loan: {
                        state: {
                            discount: 0.8
                        },
                        modules: {
                            confirm: {
                                state: {
                                    num: 2
                                }
                            }
                        }
                    },
                    product: {
                        state: {
                            discount: 0.9
                        }
                    }
                }
            }
        }
    }
    
    
    // 收集 嵌套子module state 以后的 根module state
    state: {
        name: '',
        trade: {
            loan: {
                discount: 0.8,
                confirm: {
                    num: 2
                }
            },
            product: {
                discount: 0.9
            }
        }
    }
    
  3. 使用 new Vuestore实例 构建一个 vue实例 - _vm

    构建 vue实例 的时候, 根modulestate属性 会作为 data属性 使用。

    store._vm = new Vue({
        data: {
          $$state: state  // state 为 收集子module state 以后的 根module state
        }
     });
    

    vue实例 构建完成以后, ?state每一个属性 都会变成一个 响应式属性,拥有 gettersetter, 维护一个 deps列表

    vue组件 中,我们可以通过 this.$store.state 的方式访问 _vm实例 中的 ?state属性,原理如下:

    class Store {
        ...
        // 访问 store实例的 state属性,实质为访问 store._vm._data.$$state 属性
        get state () {
            return this._vm._data.$$state
        }
    }
    

    如果 template模板computed属性watcher属性 中通过 this.$store.state 的方式使用了 某一个vuex状态, 会 触发 vuex状态 在 _vm.?data 中的 同名属性 的 getter, 对应的 watcher 会添加到 同名属性deps列表 中。 当 修改vuex状态 时, 触发vuex状态在 _vm.?data 中的 同名属性 的 setter,通知 deps列表 中的 watcher更新,然后 重新渲染界面返回新的计算属性的值触发监听callback

getter工作原理

getter,可以认为是 store计算属性。就像 vue的计算属性 一样, getter的返回值会被缓存,只有 依赖的state状态 发生变化, 才会 重新计算

getter 是基于 vue的计算属性(computed) 实现的。

vue计算属性实现原理 如下:

  1. 构建 vue实例 的时候,需要一个 options配置项,包含 datapropscomputedmethods 等属性。 如果 options 中有 computed属性,需要 遍历computed中的属性,为 每一个属性 建立一个 watcher。此外,根据 每一个computed属性, 通过 defineProperty 的方式为 vue实例 建立一个 同名属性,设置 getter。 当通过 vue实例 访问 计算属性 的时候,触发getter执行计算属性对应的方法

  2. 每一个 计算属性,都对应一个 watcherwatcher 有一个标志属性: dirty。 当 dirty 属性的值为 true 时,获取 计算属性 的值, 需要 执行计算属性对应的方法并缓存返回的结果; 当 dirty 的值为 false 时,返回 缓存的值

    watcher初次构建 的时候,dirty 值为默认为 true

    执行计算属性对应的方法 以后, dirty 的值会置为 false

  3. 第一次 使用 计算属性 的时候, 由于 watcher.dirty 的值为 true,需要 使用计算属性对应的方法。执行方法的时候, 会读取 响应式属性(data、props) 的值, 触发 响应式属性getter方法计算属性watcher 会被添加到 响应式属性deps列表 中。 此外, watcher.dirty 的值会置为 false

  4. 如果依赖的响应式属性发生变化,触发 setter方法, 通知 deps列表 中的 watcher 更新。 此时 watcher.dirty 的值会置为 true下一次 使用 计算属性 的时候, 执行计算属性对应的方法重新计算

  5. 如果依赖的响应式属性没有变化watcher.dirty 的值一直为 false下一次 使用 计算属性 的时候,直接返回 缓存的上一次计算结果

vuexgetter, 也是在使用 new Vuex.store(options) 构建 store实例 的时候 实现 的,经历的主要过程如下:

  1. 建立 根module子modules

  2. 子modules 中的 state 收集到 根modulestate 中。

  3. 根module子modules 中的 getters 收集到 store实例_wrappedGetters 对象中。

  4. store实例_wrappedGetters 对象作为 computed配置项根modulestate对象 作为 data配置 项构建 vue实例 - _vm

    store._vm = new Vue({
        data: {
          $$state: state  // state 为 收集子module state 以后的 根module state
        },
        computed: _wrappedGetters
     });
    
  5. store实例 添加 getters 属性。 遍历 _wrappedGetters 中属性,通过 defineProperty 的方式,为 store.getters 添加属性,并设置 getter方法,使我们可以通过 store.getters.xx 的方式访问 store._vm同名计算属性

当我们在 组件 中通过 this.$store.getters.xx 的方式访问 vuex 定义的 getter 时, 等价于访问 this.$store._vm 中的 同名计算属性

第一次 访问 vuex 的 getter 时,同名计算属性 对应的 watcher.dirty 的值为 true需要执行计算属性对应的方法。 执行的时候, 会 读取依赖的state的值触发state的getter方法,然后读取 this.$store._vm._data.?data同名响应式属性 的值,触发响应式属性的getter方法。此时,同名计算属性watcher 会被添加到 响应式属性的deps列表 中。同名计算属性 对应的方法执行完毕以后,结果会 缓存watcher.dirty 的值置为 false

如果 vuexgetter 依赖的 state 发生变化,this.$store._vm._data.?data 中对应的 同名响应式属性setter 会被触发,然后 通知deps列表中的watcher更新vuexgetter同名计算属性watcher.dirty 的值置为 true。 下一次访问 vuexgetter 时,根据 依赖的state,返回 新的值

如果 vuexgetter 依赖的 state 一直没有发生变化,对应的 同名计算属性watcher.dirty 的值一直为 false。 下一次访问 vuexgetter 时,返回 同名计算属性缓存的结果

严格模式

使用 new Vuex.store 构建 store实例 的时候, 如果 strict配置项 的值为 true, 则启用 严格模式

启用 严格模式 以后, 如果 state变更不是由 mutation 引起 的,则会抛出 异常

直接修改state通过mutation修改state, 都是 修改state,为什么 直接修改state就会抛出异常 呢?

vuex 中, 有一段源码涉及到 严格模式,如下:

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`);
    }
  }, { deep: true, sync: true });
}

构建 store实例 的时候, 我们会为 store实例 构建一个 vue实例_vm。 如果是 严格模式,执行 enableStrictMode 方法。

enableStrictMode 方法中, _vm 会监听 ?state。 如果 ?state 发生变化,则执行 callback

不管是 直接修改state 还是 通过mutation修改state,最后都会导致 ?state 发生变化,然后 触发callback。 不同的是,使用 mutation 时,store._committing 值为 true不会抛出异常,而 直接修改 时,store._committing 值为 false会抛出异常

默认情况下, 不启用严格模式。 即如果构建 store实例* 的时候,未设置strict配置项不启用严格模式

注意: 生产环境下, 不要启用严格模式

修改 state

vuex 建议我们通过 提交mutation 的方式 修改state, 原因如下:

  1. 严格模式下(strict: ture), 如果通过 store.state.xx = xxx 的方式 修改state, 会 抛出异常

  2. 启用dev-tools(devtools: true) 后, 如果通过 store.state.xx = xxx 的方式 修改statestate的变化 无法被 dev-tools 追踪。

开启 dev-tools 以后, 当我们通过 提交mutation 的方式 修改state 时, state的变化 可以被 dev-tools 追踪到。 在 dev-toolsvuex列表 中,我们可以清晰明了的看到每次 commit 操作。

构建 store实例 的时候, dev-tools 插件会通过 store.subscribe 方法 订阅 store 的 mutation,即 注册一个 callback。 当执行 store.commit(type, payload) 时, 先触发 type 对应的 mutations, 然后再触发 callback, 通知 dev-tools 追踪 mutation。 触发 callback时,传入 mutation经过 mutation 后的状态作为参数

命名空间 - namespace

默认情况 下,模块内部的 actionmutationgetter注册在全局命名空间 的,这样使得 多个模块 能够对 同一 mutation 或 action 作出响应。

如果 希望你的模块具有更高的 封装度 和 复用性,你可以通过添加 namespaced: true 的方式使其成为带 命名空间 的模块。当 模块注册 后,它的所有 getteractionmutation 都会 自动根据模块注册的路径调整命名

命名空间 对模块 getteractionmutation 的影响, 我们通过一个 示例 来说明:

  var option = {
    // root module
    state: { name: 'root' },
    getters: { getName: state => state.name },
    mutations: { setName: (state, name) => { state.name = name }},
    actions: { setName: (context, name) => { context.commit('setName', name) }},
    modules: {
      // module A
      moduleA: {
        namespaced: true,
        state: { name: 'moduleA' },
        getters: { getName: state => state.name },
        mutations: { setName: (state, name) => { state.name = name }},
        actions: { setName: (context, name) => { context.commit('setName', name) }}
        modules: {
          // module C
          moduleC: {
            state: { name: 'moduleC' },
            getters: { getName: state => state.name },
            mutations: { setName: (state, name) => { state.name = name }},
            actions: { setName: (context, name) => { context.commit('setName', name) }}
          },
          // module D
          moduleD: {
            namespaced: true,
            state: { name: 'moduleD' },
            getters: { getName: state => state.name },
            mutations: { setName: (state, name) => { state.name = name }},
            actions: { setName: (context, name) => { context.commit('setName', name) }}
          }
        }
      },
      // module B
      moduleB: {
        state: { name: 'moduleB' },
        getters: { getName: state => state.name },
        mutations: { setName: (state, name) => { state.name = name }},
        actions: { setName: (context, name) => { context.commit('setName', name) }}
      }
    }
  }

在上面的示例中,store 被切割成 root modulemoduleAmoduleBmoduleCmoduleD

root module,不管 namespced 的值为 true 或者 false, 它的 命名空间 都是 , 即为 全局命名空间

moduleAnamespaced 的值为 true, 它的 命名空间'moduleA'

moduleBnamespaced 的值 不是true, 它会 继承 父module 即 root module 的 命名空间,即它的 命名空间全局命名空间

moduleCnamespaced 的值 不是true, 它会 继承 父module 即 moduleA 的 命名空间,即它的 命名空间'moduleA'

moduleDnamespaced 的值为 true, 它的 命名空间'moduleA/moduleD'

命名空间gettersmutationsactions 的使用会有 影响

  • getter

    在构建 store实例 的时候,会将各个 modulegetters 收集到 store._wrappedGetters 中。

    store._wrappedGetters 是一个 对象属性名 为:命名空间/getter名, 对应的 属性值是一个 函数 - 经过包装以后的getter

    由于 _wrappedGetters 是一个 对象, 如果 存在属性名相同的getter,会抛出警告

    在上面的示例中, _wrappedGetters 中, 收集到的 getter 如下:

        _wrappedGetters = {
            'getName': function() {...},  // 根 module 中的 getter
            'moduleA/getName': function() {...}, // moduleA 的 getter
            'moduleA/moduleD/getName': function() {...} // moduleD 的 getter 
        }
    

    对于 moduleB, 命名空间为 全局命名空间,则对应的属性名为 getName, 此时 _wrappedGetters 中已经存在 同名属性无法添加且抛出警告

    moduleC 的情况也一样,命名空间为 moduleA, 对应的属性为 moduleA/getName_wrappedGetters 中已经存在 同名属性无法添加且抛出警告

    组件 中,我们可以通过 store.getters.xx 访问 getterxx 会对应 _wrappedGetters 中的 属性名, 如下:

        this.$store.getters.getName  // 访问 根module 的 getName
        
        this.$store.getters['moduleA/getName']  // 访问 moduleA 的 getName
        
        this.$store.getters['moduleA/moduleD/getName'] // 访问 moduleD 的 getName
    

    moduleBmoduleCgetName 没有收集到 _wrappedGetters 中,在 组件无法访问

  • mutation

    在构建 store实例 的时候,会将各个 modulemutations 收集到 store._mutations 中。

    store._mutations 是一个 对象属性名 为:命名空间/mutation名, 对应的 属性值数组数组元素经过包装的mutation

    在上面的示例中, _mutations 中, 收集到的 mutations 如下:

        _mutations: {
            // fn_root 对应 根module 的 setName, fnB 对应 moduleB 的 setName
            'setName': [fn_root, fnB],
            // fnA 对应 moduleA 的 setName, fnC 对应 moduleC 的 setName
            'moduleA/setName': [fnA, fnC],
            // fnD 对应 moduleD 的 setName
            'moduleA/moduleD/setName': [fnD]
        }
    

    moduleB命名空间全局命名空间,对应的属性名为 setName, 在 _mutations 已存在, 将 mutation 添加到 setName 对应的 数组中。

    moduleC命名空间moduleA,对应的属性名为 moduleA/setName, 在 _mutations 已存在, 将 mutation 添加到 moduleA/setName 对应的 数组中。

    组件 中,我们可以通过 store.commit(type, payload) 的方式 提交mutationtype 会对应 _mutations 中的 属性名,如下:

        // 触发 根module、moduleB 的 setName
        this.$store.commit('setName', 'xxx') 
        // 触发 moduleA、 moduleC 的 setName
        this.$store.commit('moduleA/setName', 'xxx')
        // 触发 moduleD 的 setName
        this.$store.commit('moduleA/moduleD/setName', xxx)
    

    在上面的示例中, root modulemoduleB命名空间 相同,提交 'setName' 时,root modulemoduleB 中的 mutation - setName 都会触发; moduleAmoduleC命名空间 相同, 提交'moduleA/setName' 时, moduleAmoduleC 中的 mutation - setName 都会触发。

  • actions

    在构建 store实例 的时候,会将各个 moduleaction 收集到 store._actions 中。

    store._actions 是一个 对象属性名 为:命名空间/action名, 对应的 属性值数组数组元素经过包装的action

    在上面的示例中, _actions 中, 收集到的 actions 如下:

        _actions: {
            // fn_root 对应 根module 的 setName, fnB 对应 moduleB 的 setName
            'setName': [fn_root, fnB],
            // fnA 对应 moduleA 的 setName, fnC 对应 moduleC 的 setName
            'moduleA/setName': [fnA, fnC],
            // fnD 对应 moduleD 的 setName
            'moduleA/moduleD/setName': [fnD]
        }
    

    moduleB命名空间全局命名空间,对应的属性名为 setName, 在 _actions 已存在, 将 action 添加到 setName 对应的 数组中。

    moduleC命名空间moduleA,对应的属性名为 moduleA/setName, 在 _actions 已存在, 将 action 添加到 moduleA/setName 对应的 数组中。

    组件 中,我们可以通过 store.dispatch(type, payload) 的方式 派发 actiontype 会对应 _actions 中的 属性名,如下:

        // 触发 根module、moduleB 的 setName
        this.$store.dispatch('setName', 'xxx') 
        // 触发 moduleA、 moduleC 的 setName
        this.$store.dispatch('moduleA/setName', 'xxx')
        // 触发 moduleD 的 setName
        this.$store.dispatch('moduleA/moduleD/setName', xxx)
    

    在上面的示例中, root modulemoduleB命名空间 相同,派发 'setName' 时,root modulemoduleB 中的 action - setName 都会触发; moduleAmoduleC命名空间 相同, 派发 'moduleA/setName' 时, moduleAmoduleC 中的 action - setName 都会触发。

state的使用

通常,我们会在 vue组件 中通过 计算属性 来访问所需的 状态 - state

具体的方式,我们通过一个 示例 来说明。

var options = {
    state: {
        name: 'zhangsan'
    },
    modules: {
        trade: {
            namespaced: true,
            state: {
                type: 'waiting'
            },
            modules: {
                product: {
                    namespaced: true,
                    state: {
                        id: 1
                    }
                }
            }
        },
        loan: {
            namespaced: true,
            state: {
                money: 100000
            }
        }
    }
}
  • 直接访问

    我们可以通过 vm.$store.state.xx 的方式直接访问所需的 状态 - state

    var bill = {
        data() {
            return {...}
        },
        computed: {
            name() {
                return this.$store.state.name
            },
            tradeType() {
                return this.$store.state.trade.type
            },
            productId() {
                return this.$store.state.trade.product.id  
            },
            loanMoney() {
                return parseInt(this.$store.state.loan.money / 1000)
            }
        }
    }
    
  • 通过 mapState 辅助函数

    我们可以使用 辅助函数 - mapState 帮助我们生成 计算属性

    var mapState = Vuex.mapState
    
    var bill = {
        data() {
            return {...}
        },
        computed: {
            ...mapState({   // 全局命名空间
                name: 'name',
                tradeType: state => state.trade.type // 全局 state
            }),
            ...mpaState('trade/product', { // trade/product 命名空间
                productId: 'id'
            }),
            ...mapState('loan', {  // loan 命名空间
                loanMoney: state => parseInt(state.money / 1000) // 局部 state
            })
        }
    }
    
  • 使用 createNamespacedHelpers 辅助函数

    我们可以使用 辅助函数 - createNamespacedHelpers、mapState 帮助我们生成 计算属性

    createNamespacedHelpers 可以帮助我们生成 基于命名空间辅助函数

    // 基于 全局 命名空间 的 mapState
    var mapState = Vuex.mapState
    // 基于 trade 命名空间 的 mapState
    var tradeMapState = Vuex.createNamespacedHelpers('trade').mapState
    // 基于 trade/product 命名空间 的 mapState
    var productMapState = Vuex.createNamespacedHelpers('trade/product').mapState
    // 基于 loan 命名空间的 mapState 
    var loanMapState = Vuex.createNamespacedHelpers('loan').mapState
    
    var bill = {
        data() {
            return {...}
        },
        computed: {
            ...mapState(['name']), // 计算属性名为 name, 返回值为 this.$store.state.name
            ...tradeMapState({
                tradeType: 'type' // 计算属性名为 tradeType, 返回值为 this.$store.state.trade.type
            }),
            ...productMapState({
                productId: 'id'
            }),
            ...mapState({
                loanMoney: state => parseInt(state.money / 1000)
            })
        }
    }
    

getter的使用

vuexgetter 的使用,和 state 类似,我们同样以一个示例来说明。

var options = {
    state: {
        name: 'zhangsan'
    },
    getters: {
        getName(state) {
            return state.name
        }
    },
    modules: {
        trade: {
            namespaced: true,
            state: {
                type: 'waiting'
            },
            getters: {
              getTradeType(state) {
                  return state.type
              }  
            },
            modules: {
                product: {
                    namespaced: true,
                    state: {
                        id: 1
                    },
                    getters: {
                        getProductId(state) {
                            return state.id
                        }
                    }
                }
            }
        },
        loan: {
            namespaced: true,
            state: {
                money: 100000
            },
            getters: {
                getLoanMoney(state) {
                    return state.money
                }
            }
        },
        confirm: {
            state: {
                num: 100
            },
            getters: {
                getConfirmNum(state) {
                    return state.num
                }
            }
        }
    }
}
  • 直接使用

    我们可以通过 vm.$store.getters.xx 的方式直接访问所需的 getter

    var mapGetters = Vuex.mapGetters
    
    var bill = {
        data() {
            return {...}
        },
        computed: {
            getName() {
                return this.$store.getters.getName
            },
            getTradeType() {
                return this.$store.getters['trade/getTradeType']
            },
            getLoanMoney() {
                return this.$store.getters['loan/getLoanMoney']
            },
            getProductId() {
                return this.$store.getters['trade/product/getProductId']
            },
            getConfirmNum() {
                return this.$store.getters.getConfirmNum
            }
        },
        ...
    }
    
  • 使用 mapGetters 辅助函数

    我们可以使用 辅助函数 - mapGetters 帮助我们生成 计算属性

    var bill = {
        data() {
            return {...}
        },
        computed: {
            ...mapGetters(['getName', 'getConfirmNum']),
            ...mapGetters({
                getTradeType: 'trade/getTradeType'
            }),
            ...mapGetters('trade', {
                getProductId: 'product/getProductId'
            }),
            ...mapGetters('loan', {
                'getLoanMoney': 'getLoanMoney'
            })
        },
        ...
    }
    
  • 使用 createNamespacedHelpers 辅助函数

    我们可以使用 辅助函数 - createNamespacedHelpers、mapGetters 帮助我们生成 计算属性

    createNamespacedHelpers 可以帮助我们生成 基于命名空间辅助函数

    // 基于 全局 命名空间 的 mapGetters
    var mapGetters = Vuex.mapGetters
    // 基于 trade 命名空间 的 mapGetters
    var tradeMapGetters = Vuex.createNamespacedHelpers('trade').mapGetters
    // 基于 trade/product 命名空间 的 mapGetters
    var productMapGetters = Vuex.createNamespacedHelpers('trade/product').mapGetters
    // 基于 loan 命名空间的 mapGetters 
    var loanMapGetters = Vuex.createNamespacedHelpers('loan').mapGetters
    
    var bill = {
        data() {
            return {...}
        },
        computed: {
            ...mapGetters(['getName', 'getConfirmNum']),
            ...tradeMapGetters({
                getTradeType: 'getTradeType'
            }),
            ...productMapGetters(['getProductId']),
            ...loanMapGetters(['getLoanMoney'])
        }
    }
    

mutation的使用

mutation 用于 更改 vuex 的 state, 它的用法和 stategetter 类似,我们同样以一个示例来说明。

var options = {
    state: {
        name: 'zhangsan'
    },
    mutations: {
        setName(state, name) {
            state.name = name
        }
    },
    modules: {
        trade: {
            namespaced: true,
            state: {
                type: 'waiting'
            },
            mutations: {
                setTradeType(state, type) {
                    state.type = type
                }
            }
            modules: {
                product: {
                    namespaced: true,
                    state: {
                        id: 1
                    },
                    mutations: {
                        setProductId(state, id) {
                            state.id = id
                        }
                    }
                }
            }
        },
        loan: {
            namespaced: true,
            state: {
                money: 100000
            },
            mutations: {
                setLoanMoney(state, money) {
                    state.money = money
                }
            }
        },
        confirm: {
            state: {
                num: 10
            },
            mutations: {
                setConfirmNum(state, num) {
                    state.num = num
                }
            }
        }
    }
}
  • 直接使用

    组件中,我们可以通过 this.$store.commit(type, payload) 的方式提交 mutation,触发 state状态更改

    var bill = {
        ...
        methods: {
            setName() { 
                this.$store.commit('setName', '李四') 
            },
            setTradeType() { 
                this.$store.commit('trade/setTradeType', 'complete') 
            },
            setProductId() {
                this.$store.commit('trade/product/setProductId', '2')
            },
            setLoanMoney() {
                this.$store.commit('loan/setLoanMoney', '2000000')
            },
            setConfirmNum() {
                this.$store.commit('setConfirmNum', '20')
            }
        }
    }
    
  • 使用 mapMutations 辅助函数

    我们可以使用 辅助函数 - mapMutations 帮助我们生成 组件方法 来提交 mutation,触发 state状态更改

    var mapMutations = Vuex.mapMutations
    
    var bill = {
        ...
        methods: {
            // 给 bill组件 添加 setName 方法, 映射 this.$store.commit('setName')
            // 给 bill组件 添加 setConfirmNum 方法, 映射 this.$store.commit('setConfirmNum')
            ...mapMutations(['setName', 'setConfirmNum']),
            // 给 bill组件 添加 setTradeType 方法, 映射 this.$store.commit("trade/setTradeType')
            ...mapMutations({
                setTradeType: 'trade/setTradeType'
            }),
            // 给 bill组件 添加 setProductId, 映射 this.$store.commit('trade/product/setProductId')
            ...mapMutations('trade', {
                setProductId: 'product/setProductId'
            }),
            // 给 bill组件 添加 setLoanMoney, 映射 this.$store.commit('loan/setLoanMoney') 
            ...mapMutations('loan', {
                setLoanMoney: function(commit, money) {
                    commit('setLoanMoney', money)
                }
            }),
            submit() {
                // this.$store.commit('setName', '李四')
                this.setName('李四')
                // this.$store.commit('setConfirmNum', 20)
                this.setConfirmNum(20)
                // this.$store.commit('trade/setTradeType', 'complete')
                this.setTradeType('complete')
                // this.$store.commit('trade/product/setProductId', 2345)
                this.setProductId('2345')
                // this.$store.commit('loan/setLoanMoney', 2000000) 
                this.setLoanMoney(20000000)
            }
    
        }
    }
    
  • 使用 createNamespacedHelpers 辅助函数

    我们可以使用 辅助函数 - createNamespacedHelpers、mapMutations 帮助我们生成 生成 组件方法 来提交 mutation,触发 state状态更改。。

    createNamespacedHelpers 可以帮助我们生成 基于命名空间辅助函数

    // 基于 全局 命名空间 的 mapMutations
    var mapMutations = Vuex.mapMutations
    // 基于 trade 命名空间 的 mapMutations
    var tradeMapMutations = Vuex.createNamespacedHelpers('trade').mapMutations
    // 基于 trade/product 命名空间 的 mapMutations
    var productMapMutations = Vuex.createNamespacedHelpers('trade/product').mapMutations
    // 基于 loan 命名空间的 mapMutations 
    var loanMapMutations = Vuex.createNamespacedHelpers('loan').mapMutations
    
    var bill = {
        ...
        methods: {
            // 给 bill组件 添加 setName 方法, 映射 this.$store.commit('setName')
            // 给 bill组件 添加 setConfirmNum 方法, 映射 this.$store.commit('setConfirmNum')
            ...mapMutations(['setName', 'setConfirmNum']),
            // 给 bill组件 添加 setTradeType 方法, 映射 this.$store.commit('trade/setTradeType')
            ...tradeMapMutations(['setTradeType']),
            // 给 bill组件 添加 setProductId, 映射 this.$store.commit('trade/product/setProductId')
            ...productMapMutations(['setProductId']),
            // 给 bill组件 添加 setLoanMoney, 映射 this.$store.commit('loan/setLoanMoney') 
            ...loanMapMutations({
                setLoanMoney: function(commit, money) {
                    commit('setLoanMoney', money)
                }
            }),
            submit() {
                // this.$store.commit('setName', '李四')
                this.setName('李四')
                // this.$store.commit('setConfirmNum', 20)
                this.setConfirmNum(20)
                // this.$store.commit('trade/setTradeType', 'complete')
                this.setTradeType('complete')
                // this.$store.commit('trade/product/setProductId', 2345)
                this.setProductId('2345')
                // this.$store.commit('loan/setLoanMoney', 2000000) 
                this.setLoanMoney(20000000)
            }
        }
    }
    

action的使用

action 类似于 mutation,不同之处在于:action 提交的是 mutation,而 不是直接变更状态action 可以 包含任意异步操作

action组件 中的使用, 和 mutation 类似,我们通过一个 示例 来说明:

var options = {
    state: {
        name: 'zhangsan'
    },
    mutations: {
        setName(state, name) {
            state.name = name
        }
    },
    actions: {
        setName(context, name) {
            setTimeout(() => {
                context.commit('setName', name)
            }, 0)
        }
    }
    modules: {
        trade: {
            namespaced: true,
            state: {
                type: 'waiting'
            },
            mutations: {
                setTradeType(state, type) {
                    state.type = type
                }
            }
            actions: {
                setTradeType(context, type) {
                    setTimeout(() => {
                        context.commit('setTradeType', type)
                    }, 1500)
                }
            }
            modules: {
                product: {
                    namespaced: true,
                    state: {
                        id: 1
                    },
                    mutations: {
                        setProductId(state, id) {
                            state.id = id
                        }
                    },
                    actions: {
                        setProductId(context, id) {
                            setTimeout(() => {
                                context.commit('setProductId', id)
                            }, 3000)
                        }
                    }
                }
            }
        },
        loan: {
            namespaced: true,
            state: {
                money: 100000
            },
            mutations: {
                setLoanMoney(state, money) {
                    state.money = money
                }
            },
            actions: {
                setLoanMoney(context, money) {
                    setTimeout(() => {
                        context.commit('setLoanMoney', money)
                    }, 2000)
                }
            }
        },
        confirm: {
            state: {
                num: 10
            },
            mutations: {
                setConfirmNum(state, num) {
                    state.num = num
                }
            },
            actions: {
                setConfirmNum(context, num) {
                    setTimeout(() => {
                        context.commit('setConfirmNum', num)
                    }, 5000)
                }
            }
        }
    }
}
  • 直接使用

    在组件中,我们可以直接通过 this.$store.dispatch(type, payload) 的方式 派发 action,触发 mutaion提交

    var bill = {
        ...
        methods: {
            setName() { 
                this.$store.dispatch('setName', '李四') 
            },
            setTradeType() { 
                this.$store.dispatch('trade/setTradeType', 'complete') 
            },
            setProductId() {
                this.$store.dispatch('trade/product/setProductId', '2')
            },
            setLoanMoney() {
                this.$store.dispatch('loan/setLoanMoney', '2000000')
            },
            setConfirmNum() {
                this.$store.dispatch('setConfirmNum', '20')
            }
        }
    }
    
  • 使用 mapActions 辅助函数

    我们可以使用 辅助函数 - mapActions 帮助我们生成 组件方法 来派发 action,触发 mutation提交

    var mapActions = Vuex.mapActions
    
    var bill = {
        ...
        methods: {
            // 给 bill组件 添加 setName 方法, 映射 this.$store.dispatch('setName')
            // 给 bill组件 添加 setConfirmNum 方法, 映射 this.$store.dispatch('setConfirmNum')
            ...mapActions(['setName', 'setConfirmNum']),
            // 给 bill组件 添加 setTradeType 方法, 映射 this.$store.dispatch("trade/setTradeType')
            ...mapActions({
                setTradeType: 'trade/setTradeType'
            }),
            // 给 bill组件 添加 setProductId, 映射 this.$store.dispatch('trade/product/setProductId')
            ...mapActions('trade', {
                setProductId: 'product/setProductId'
            }),
            // 给 bill组件 添加 setLoanMoney, 映射 this.$store.dispatch('loan/setLoanMoney') 
            ...mapActions('loan', {
                setLoanMoney: function(dispatch, money) {
                    dispatch('setLoanMoney', money)
                }
            }),
            submit() {
                // this.$store.dispatch('setName', '李四')
                this.setName('李四')
                // this.$store.dispatch('setConfirmNum', 20)
                this.setConfirmNum(20)
                // this.$store.dispatch('trade/setTradeType', 'complete')
                this.setTradeType('complete')
                // this.$store.dispatch('trade/product/setProductId', 2345)
                this.setProductId('2345')
                // this.$store.dispatch('loan/setLoanMoney', 2000000) 
                this.setLoanMoney(20000000)
            }
    
        }
    }
    
    
    
  • 使用 createNamespacedHelpers 辅助函数

    我们可以使用 辅助函数 - createNamespacedHelpers、mapActions 帮助我们生成 生成 组件方法 来派发 action,触发 mutation提交。。

    createNamespacedHelpers 可以帮助我们生成 基于命名空间辅助函数

    // 基于 全局 命名空间 的 mapActions
    var mapActions = Vuex.mapActions
    // 基于 trade 命名空间 的 mapActions
    var tradeMapActions = Vuex.createNamespacedHelpers('trade').mapActions
    // 基于 trade/product 命名空间 的 mapActions
    var productMapActions = Vuex.createNamespacedHelpers('trade/product').mapActions
    // 基于 loan 命名空间的 mapActions 
    var loanMapActions = Vuex.createNamespacedHelpers('loan').mapActions
    
    var bill = {
        ...
        methods: {
            // 给 bill组件 添加 setName 方法, 映射 this.$store.dispatch('setName')
            // 给 bill组件 添加 setConfirmNum 方法, 映射 this.$store.dispatch('setConfirmNum')
            ...mapActions(['setName', 'setConfirmNum']),
            // 给 bill组件 添加 setTradeType 方法, 映射 this.$store.dispatch('trade/setTradeType')
            ...tradeMapActions(['setTradeType']),
            // 给 bill组件 添加 setProductId, 映射 this.$store.dispatch('trade/product/setProductId')
            ...productMapActions(['setProductId']),
            // 给 bill组件 添加 setLoanMoney, 映射 this.$store.dispatch('loan/setLoanMoney') 
            ...loanMapActions({
                setLoanMoney: function(dispatch, money) {
                    dispatch('setLoanMoney', money)
                }
            }),
            submit() {
                // this.$store.dispatch('setName', '李四')
                this.setName('李四')
                // this.$store.dispatch('setConfirmNum', 20)
                this.setConfirmNum(20)
                // this.$store.dispatch('trade/setTradeType', 'complete')
                this.setTradeType('complete')
                // this.$store.dispatch('trade/product/setProductId', 2345)
                this.setProductId('2345')
                // this.$store.dispatch('loan/setLoanMoney', 2000000) 
                this.setLoanMoney(20000000)
            }
        }
    }
    

其他

  1. 不能通过 store.state = xx 方式直接修改 state, 否则会 抛出异常

    class Store {
        ...
        // 访问 store实例的 state属性,实质为访问 store._vm._data.$$state 属性
        get state () {
            return this._vm._data.$$state
        }
        set state (v) {
            {
              assert(false, `use store.replaceState() to explicit replace store state.`);
            }
        }
        ...
        replaceState (state) {
            this._withCommit(() => {
              this._vm._data.$$state = state;
            });
        }
    }
    

    为什么不能在 set state 中, 使用 replaceState中的方式修改 state?? 暂未理解。

  2. 通过 store.watch 可以 响应式地侦听 fn 的返回值,当值改变时 调用callback

    fn 接收 storestate 作为第一个参数,其 getter 作为第二个参数, 只要 vuex状态 发生 变化fn 都会执行, 如果 返回的值和上一次不一样, 触发 callback

  3. 通过 store.subscribe 方法可以 订阅 store 的 mutation。 当我们通过 store.commit(type, payload) 的方式提交 mutation 时, 先触发 type 对应的 mutation, 再触发 subscribe 注册的 handler

    vue-devtools 就是利用 store.subscribe 来追踪 应用中提交的 mutation

    store.subscribe((mutation, state) => {
        // vue-devtools 提供的 devtoolHook
        devtoolHook.emit('vuex:mutation', mutation, state);
    });
    

    store.subscribe 一般被 插件 调用。

  4. 通过 store.subscribeAction 方法可以订阅 storeaction。 用法如下:

    store.subscribeAction({
        before: function(action, state) {
            ...
        },
        after: function(action, state) {
            ...
        }
    })
    

    当我们通过 store.dispatch(type, payload) 的方式派发 action 时, 先触发 before 对应的 handler,再触发 type 对应的 mutation, 最后触发 after 对应的 handler

    如果 subscribeAction 的参数是一个 函数, 默认当做 before 对应的 handler, 即:

    store.subscribeAction(function(action, state) {
        ...
    })
    
  5. 通过 store.registerModule 方法可以 动态注册一个 新模块

    registerModule第一个参数,代表 注册的模块名及所在的父module第二个参数注册module需要的配置项, 即 { namespaced, state, getters, actions, mutations, modules }

    // 在 根module 中注册 moduleA
    store.registerModule('moduleA', {...})
    // 在 根module 中注册 moduleA
    store.registerModule(['moduleA'], {...})
    // 在 moduleB 中注册 moduleC
    store.registerModule(['moduleA', 'moduleB', 'moduleC'],  {...})
    

    注意,在第三个示例中, 如果 moduleA根module 中不存在, moduleBmoduleA 中不存在, 会抛出 异常

    动态注册一个新模块以后, store实例会重新收集state、getters、mutations、actions, 重新为store实例构建vm实例(响应式原理)

  6. 通过 store.unregisterModule 方法可以卸载一个动态模块。

    unregisterModule 只有 一个参数, 代表需要 卸载的模块及所在的父module

    // 在 根module 中卸载 moduleA
    store.unregisterModule('moduleA')
    // 在 根module 中卸载 moduleA
    store.unregisterModule(['moduleA'])
    // 在 moduleB 中卸载 moduleA
    store.unregisterModule(['moduleA', 'moduleB', 'moduleC'])
    

    卸载的模块必须是动态注册的模块,否则抛出异常。 原因是: 卸载静态module 时, 静态module 不会从 父module 中移除, 但是 静态module 对应的 state 会被移除, 如果 vue组件 中有使用 静态module 中的 state,会报错。 这里应该是vux的一个bug吧。

    如果 要卸载的模块在父模块中不存在,会 报错。 这里应该也是一个bug。

    如果 要卸载的模块的父模块的路劲不对, 会 报错

    卸载一个动态注册的模块以后, store实例会重新收集state、getters、mutations、actions, 重新为store实例构建vm实例(响应式原理)

  7. devtools 配置项

    为 store实例 打开或者关闭 devtools

    如果将 devtools 设置为 falsevue-devtools 就无法通过 store.subscribe 方法 订阅 store 的 mutation无法追踪store提交的mutation