Skip to content

Store

对于 Vuex 来说 Store 是其核心,其作为一个中央仓库,提供了一下的功能:

  1. 层级 module 的数据存储功能
  2. 创建实例 store 对象维护整个功能

Store 初始化过程

js
const store = new Vuex.Store({
  modules: {
    xxx,
  },
})

new Vue({
  store,
  template: ``,
}).$mount("#app")

Store 对象

Store 是一个 Class 对象,其内置了很多的内部变量去存储和维护整个 module 功能,如

  • _modules , _modulesNamespaceMap 维护 module 的
  • _actions , _actionSubscribers 维护 action 的
  • _wrappedGetters 维护 getter 的
  • _mutations 维护 mutations 的
  • _committing 维护 commit 的状态

也提供了我们开发过程中需要的几个核心属性和方法: statecommitdispatch

我们先简单的看一下一个 store 对于我们整个 Vuex 数据的存储结构

js
store = {
    _commiting : false,
    // 存放整个module树
    _modules :{
        root : {      // Module
            a: xxx    // Module
        }
    },
    // 按照模块全路径保存所有的局部命名 的 模块
    _modulesNamespaceMap : {
        ‘a/’ : ModuleA,
        'a/aa' : ModuleAA
    },
    _mutations : {
        'a/aa/commit1' : function
    },
    _actions : {
        'a/aa/action1' : function
    },
    _wrappedGetters : {
        'a/aa/getter1' : function
    },
    // 通过一个computed 去 处理所有的计算属性的依赖关系
    _vm : {            // Vue实例对象  主要关注 data属性和 computed属性
        data : {
            $$state : store
        },
        computed:{
            ... store._wrappedGetters
        }
    },
    // 两个实例方法
    commit  : function ,
    dispatch : function ,
    getters  : {}

}

从上面的结果我们可以看出对于 Vuex 的树形结构 Module 的功能其转换的结果并不是按照 moduleA.moduleAA...的方式去存储的,而是将子孙 module 的 mutation、getter、action 按照特殊的命名方式统一存放在 store 实例对象下,结构如下 a/aa/commit1a/aa/action1a/aa/getter1

installModule

核心代码

js
/**
  加载Module 在 new ModuleConllection的时候 初始化处理了整个module树,那么这时候处理module树中的 state、mutation 、 action...

  重点 : getter action mutation 路径是如何处理的?

  我们知道Module中存在 namespaced属性,如果为 true , 使其成为带命名空间的模块. 所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。


 * @param {*} store                 // store对象
 * @param {*} rootState             // 跟state
 * @param {*} path                  // 模块路径 ['a','ab']
 * @param {*} module                // 模块实例化对象
 * @param {*} hot                   // 是否保留原来的state
 */
function installModule(store, rootState, path, module, hot) {
  // 在Store中调用installModule() 中 installModule(this, state, [], this._modules.root)
  // path = [];
  // 然后深度遍历 module树的时候 path.concat(key) 使得变成 [ 'a' , 'aa']
  // 如果path.length === 0 说明这是 根模块
  const isRoot = !path.length

  // 调用module 的 getNamespace 然后根据子模块 namespaced 去形成 各模块的路径
  //  [ 'a' , 'aa' , 'aaa'] 中 全有 namespaced:true ,           a => "a"; 'aa' => 'a/aa'; 'aaa' => 'a/aa/aaa'
  //  [ 'a' , 'ab' , 'aba'] 中 全有 'ab' 的 namespaced:false ,  a => "a"; 'ab' => 'a'; 'aba' => 'a/aba'
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  // 如果当前模块是局部命名空间,那么就需要将当前模块注册到 store._modulesNamespaceMap中
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    // 获取上一级module的 state对象
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 获取当前module的name key
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 通过Vue.set 响应式的将当前state存放到上一级state.moduleName下
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = (module.context = makeLocalContext(store, namespace, path))

  //  在store上添加
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 处理 modules中的 actions 属性
  // 如: const actions = { increment: ({ commit }) => commit('increment'),decrement: ({ commit }) => commit('decrement')}
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 通过判断是否存在子 module,然后不断深度遍历从而处理整个 module 树
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

重点:

state 的存储

代码为

js
// set state
if (!isRoot && !hot) {
  // 获取上一级module的 state对象
  const parentState = getNestedState(rootState, path.slice(0, -1))
  // 获取当前module的name key
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    // 通过Vue.set 响应式的将当前state存放到上一级state.moduleName下
    Vue.set(parentState, moduleName, module.state)
  })
}

可以见得对于 state 树的结构为一下方式

modueleA: {
  modueleAB : {
    modueleABA
  }
  modueleAC:{
    modueleACA
  }
}

结果变成

js
state = {
  a1: 1,
  a2: 2,
  // moduleAB
  modueleAB: {
    ab1: 1,
    ab2: 1,
    // modueleABA
    modueleABA: {
      aba1: 1,
      aba2: 1,
    },
  },
  // moduleAC
  modueleAC: {
    ac1: 1,
    // modueleACA
    modueleACA: {
      aca1: 1,
    },
  },
}

action 和 getter 的存储

这两个数据信息的存储跟 state 不一样,其不再是按照树形的结构去存储信息了,而是将其扁平化到 store 中去,不需要进行深层嵌套获取

action

modueleA: {
  modueleAB : {
    modueleABA
  }
  modueleAC:{
    modueleACA
  }
}

结果变成

js
store = {
  "moduleA/action" : () => {}
  "moduleA/modueleAB/action" : () => {}
  "moduleA/modueleAB/modueleABA/action" : () => {}
  "modueleAC/action" : () => {}
  "modueleAC/modueleACA/action" : () => {}
}
  1. module 树的处理

通过判断是否存在子 module,然后不断深度遍历从而处理整个 module 树

命名空间维护

对于 Vuex 其命名空间的维护是比较重要的一点,通过判断是否存在 namespaced : true 去将整个 module 树转换成一个个 path 路径数组结构

例子

js
modueleA: {
  namespaced: true,
  modueleAB : {
    namespaced: true,
    modueleABA :{
      namespaced: true,
    },
     modueleABB :{
      namespaced: true,
    }
  },
  modueleAC: {
    namespaced: true,
    modueleACA :{
      namespaced: true,
    },
    modueleACB :{
      namespaced: false,
    }
  }
}

其转换后的结构如下:

js
modueleA = "modueleA"
modueleAB = "modueleA/modueleAB"
modueleABA = "modueleA/modueleAB/modueleABA"
modueleABB = "modueleA/modueleAB/modueleABB"
modueleAC = "modueleA/modueleAC"
modueleACA = "modueleA/modueleAC/modueleACA"
modueleACB = "modueleA/modueleAC"

核心代码

js
// 根据 各模块的 namespace 形成模块的路径
/**
 * [ 'a' , 'aa' , 'aaa'] 中 全有 namespaced:true ,
 * [ 'a' , 'ab' , 'aba'] 中 'ab' 的 namespaced:false ,  a => "a"; 'ab' => 'a'; 'aba' => 'a/aba'
 * @param {*} path
 */
function ModuleCollection.getNamespace(path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + "/" : "")
  }, "")
}

如上述所示 我们在组件中使用 action 的时候那就可以使用 this.$store.dispatch("modueleA/modueleAB/actionxx" , {})去访问对应的 action 方法了,那么在 module 的 action 中我们去调用例外的 action 是否也需要全路径的方式去访问哪?对于这个问题 Vuex 是如何处理的

Vuex 对于组件中访问 和 module 访问其实提供了不同的方法,其实现如下

  • 组件中访问 action
js
this.dispatch = function boundDispatch(type, payload) {
  return dispatch.call(store, type, payload)
}
  • module 中访问 action
js
// 根据namespaced提供其全路径的调用方法对象
const local = (module.context = makeLocalContext(store, namespace, path))

let res = handler.call(
  store,
  {
    // 调用本地的 local
    dispatch: local.dispatch,
    commit: local.commit,
    getters: local.getters,
    state: local.state,
    rootGetters: store.getters,
    rootState: store.state,
  },
  payload,
  cb
)

可见其核心方法就是 makeLocalContext

makeLocalContext

提供不需要全路径的方式去访问本地 module 其他 action,getter 的方法的功能

核心源码

js
function makeLocalContext(store, namespace, path) {
  // 判断是否是根模块
  const noNamespace = namespace === ""

  const local = {
    /*
            生成根模块和各局部命名空间模块 的 dispatch方法,使得在局部命名空间模块 不需要全路径调用
            也是提供我们在 模块action中 
            increment: ({ dispatch,commit,getters,state,rootGetters,rootState } , payload, cb ) => commit('increment')
            dispatch去访问模块中其他的action
            dispatch('rootAction',{ name:'xx'},{ root: false})   // root:true去访问全局的action
            ===
            dispatch('a/ab/rootAction',{ name:'xx'},{ root: true})
         */
    dispatch: noNamespace
      ? store.dispatch
      : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          const { payload, options } = args
          let { type } = args
          if (!options || !options.root) {
            type = namespace + type
            if (process.env.NODE_ENV !== "production" && !store._actions[type]) {
              console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
              return
            }
          }
          return store.dispatch(type, payload)
        },

    /*
            生成根模块和各局部命名空间模块 的 commit方法,使得在局部命名空间模块 不需要全路径调用
            也是提供我们在 模块action中 
            increment: ({ dispatch,commit,getters,state,rootGetters,rootState } , payload, cb ) => commit('increment')
            commit去访问各自模块中其他的mutation

            commit('decrement',{ name:'xx'},{ root: false})   
            ===
            commit('a/ab/decrement',{ name:'xx'},{ root: true})    // root:true去访问全局的commit
         */
    commit: noNamespace
      ? store.commit
      : (_type, _payload, _options) => {
          const args = unifyObjectStyle(_type, _payload, _options)
          // 获取参数和配置
          const { payload, options } = args
          // 获取提交的路径
          let { type } = args
          // 判断是否 显式设置 commit 提交到根模块而不是当前模块  options.root : true
          if (!options || !options.root) {
            // 如果没有设置 root ,那么说明提交到当前模块,那么我们的 commit1 就需要添加他的命名路径 变成 'a/aa/aaa/commit1'
            type = namespace + type
            if (process.env.NODE_ENV !== "production" && !store._mutations[type]) {
              console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
              return
            }
          }
          // 还是调用store中 按照全路径使用
          store.commit(type, payload, options)
        },
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace),
    },
    state: {
      get: () => getNestedState(store.state, path),
    },
  })

  return local
}