剖析 Vuex 源码中 `Module` (模块) 的递归注册和命名空间 (namespaced) 解析机制。

各位听众,大家好!今天咱们来聊聊 Vuex 的一个核心概念——模块(Module),以及它那让人又爱又恨的递归注册和命名空间解析机制。 别害怕,虽然听起来有点学术,但保证用大白话给你讲明白,让你看完之后也能对着源码指点江山。

一、啥是 Vuex 模块?为啥要有它?

首先,咱们得搞清楚为啥要有模块这玩意儿。 想象一下,你的 Vue 应用越来越庞大,状态越来越多,全都堆在一个 store.js 文件里,那简直就是一场噩梦。 找个变量像大海捞针,改个东西生怕影响全局,维护起来简直要崩溃。

模块就是来拯救你的。 它允许你把 Vuex 的 store 分割成多个独立的模块,每个模块都有自己的 state、mutations、actions 和 getters。 就像盖房子,你把卧室、厨房、客厅分开,各自负责自己的功能,互不干扰。

二、Module 类的真面目:存储模块信息的容器

在 Vuex 源码里,模块是通过 Module 类来表示的。 Module 类负责存储模块的所有信息,包括 state、mutations、actions、getters,以及子模块。 咱们先来看看 Module 类的基本结构(简化版):

class Module {
  constructor(rawModule, runtime) {
    this.runtime = runtime; // 是否是运行时注册的模块,动态添加的
    this._children = Object.create(null); // 子模块
    this._rawModule = rawModule; // 原始模块定义对象,用户写的
    const rawState = rawModule.state;

    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}; // 模块的 state

    this._namespaced = !!rawModule.namespaced; // 是否启用命名空间

    this._mutations = Object.create(null);
    this._actions = Object.create(null);
    this._getters = Object.create(null);
  }

  getChild(key) {
    return this._children[key]
  }

  hasChild(key) {
    return key in this._children
  }

  addChild(key, module) {
    this._children[key] = module
  }

  forEachMutation(fn) {
    forEachValue(this._mutations, fn)
  }

  forEachAction(fn) {
    forEachValue(this._actions, fn)
  }

  forEachGetter(fn) {
    forEachValue(this._getters, fn)
  }

  forEachChild(fn) {
    forEachValue(this._children, fn)
  }
}

这个 Module 类,就像一个容器,把模块的所有信息都装进去。 注意几个关键点:

  • _children: 存储子模块的容器,是一个对象,key 是子模块的名称,value 是 Module 实例。
  • _rawModule: 存储用户定义的原始模块对象,就是你写的 statemutationsactionsgetters 这些东西。
  • _namespaced: 一个 boolean 值,表示这个模块是否启用了命名空间,这个咱们后面重点讲。
  • state: 模块的状态,如果是函数则执行返回,否则直接赋值,保证了状态的响应式。
  • forEachMutation/Action/Getter/Child: 这些方法用于遍历模块的 mutations、actions、getters 和子模块,方便 Vuex 在注册模块的时候进行处理。

三、递归注册:像俄罗斯套娃一样层层嵌套

Vuex 在初始化的时候,会递归地注册模块。 啥叫递归注册? 就是说,如果一个模块有子模块,Vuex 会先注册父模块,然后递归地注册子模块,子模块的子模块,一直到最底层的模块为止。 这就像俄罗斯套娃,一个套着一个,直到最小的那个。

递归注册的核心代码在 installModule 函数里(简化版):

function installModule(store, rootState, path, module, hot) {

  const isRoot = !path.length //是否是根模块
  const namespace = store._modules.getNamespace(path) //根据路径获取命名空间

  // register in namespace map
  if (module._namespaced) {
    if (store._namespacedModules[namespace] && !hot) {
      console.error(`[vuex] duplicate namespace ${namespace} for module [${path.join('/')}]`)
    }
    store._namespacedModules[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    //获取父模块的状态,并将当前模块的状态设置为父模块的属性
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    }, true)
  }

  // register mutations
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // register actions
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

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

  // install child modules
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot) //递归调用
  })
}

这个函数做了几件事:

  1. 计算命名空间: 根据模块的路径(path)和 namespaced 属性,计算出模块的命名空间。
  2. 注册状态: 把模块的 state 注册到 Vuex 的根 state 上,如果是子模块,就注册到父模块的 state 上。
  3. 注册 mutations、actions、getters: 遍历模块的 mutations、actions、getters,把它们注册到 Vuex 的 _mutations_actions_getters 对象上。
  4. 递归注册子模块: 如果模块有子模块,就递归调用 installModule 函数,注册子模块。

重点是最后一步,递归调用 installModule 函数,这就是俄罗斯套娃的核心。 path 变量记录了当前模块的路径,每次递归调用,都会把子模块的名称添加到 path 上。 这样,Vuex 就能知道当前模块在整个模块树中的位置。

四、命名空间:模块的独立王国

命名空间是 Vuex 模块的一个重要特性。 它可以让模块更加独立,避免不同模块之间的命名冲突。 你可以把每个模块想象成一个独立的王国,每个王国都有自己的法律(mutations、actions、getters)。 如果没有命名空间,所有的王国都使用同一套法律,那肯定会乱套。

启用命名空间很简单,只需要在模块定义里加上 namespaced: true 即可:

const moduleA = {
  namespaced: true,
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    increment(context) {
      context.commit('increment')
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  }
}

启用了命名空间之后,访问模块的 mutations、actions、getters 就需要加上模块的名称作为前缀:

// 提交 moduleA 的 increment mutation
store.commit('moduleA/increment')

// 分发 moduleA 的 increment action
store.dispatch('moduleA/increment')

// 获取 moduleA 的 doubleCount getter
store.getters['moduleA/doubleCount']

五、命名空间解析:Vuex 如何找到你的模块?

Vuex 如何知道 moduleA/increment 对应的是哪个模块的哪个 mutation 呢? 这就涉及到命名空间解析的过程。

命名空间解析的核心代码在 getNamespace 函数里:

getNamespace(path) {
    let namespace = path.reduce((namespace, key) => {
      return namespace + (this.get(key)._namespaced ? key + '/' : '')
    }, '')
    return namespace
  }

这个函数接收一个模块的路径 path,然后遍历路径上的每个模块,如果模块启用了命名空间,就把模块的名称加上 / 作为前缀添加到命名空间字符串上。

举个例子,假设有以下模块结构:

const store = new Vuex.Store({
  modules: {
    moduleA: {
      namespaced: true,
      modules: {
        moduleB: {
          namespaced: false,
          modules: {
            moduleC: {
              namespaced: true
            }
          }
        }
      }
    }
  }
})

要访问 moduleC 的一个 mutation,路径就是 ['moduleA', 'moduleB', 'moduleC']getNamespace 函数会这样计算命名空间:

  1. moduleA 启用了命名空间,所以命名空间字符串变成 'moduleA/'
  2. moduleB 没有启用命名空间,所以命名空间字符串不变。
  3. moduleC 启用了命名空间,所以命名空间字符串变成 'moduleA/moduleC/'

所以,moduleC 的命名空间就是 'moduleA/moduleC/'。 Vuex 在注册 moduleC 的 mutations、actions、getters 的时候,都会加上这个前缀。

六、mapStatemapGettersmapMutationsmapActions:让你的代码更简洁

手动拼接命名空间太麻烦了,Vuex 提供了 mapStatemapGettersmapMutationsmapActions 这几个辅助函数,可以让你更方便地访问模块的状态、getters、mutations 和 actions。

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

export default {
  computed: {
    ...mapState('moduleA', ['count']),
    ...mapGetters('moduleA', ['doubleCount'])
  },
  methods: {
    ...mapMutations('moduleA', ['increment']),
    ...mapActions('moduleA', ['incrementAsync'])
  }
}

这些辅助函数会自动帮你拼接命名空间,让你的代码更简洁。

七、动态注册模块:让你的应用更灵活

Vuex 还允许你动态地注册模块,也就是在应用运行的时候添加模块。 这可以让你根据用户的行为或者应用的状态,动态地加载不同的模块。

store.registerModule('moduleC', {
  namespaced: true,
  state: () => ({ count: 0 }),
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

registerModule 函数会调用 installModule 函数,把新的模块注册到 Vuex 的 store 上。

八、总结:模块化的力量

今天咱们深入剖析了 Vuex 的模块机制,包括 Module 类的结构、递归注册的过程、命名空间的解析,以及动态注册模块。 希望通过今天的讲解,你能更好地理解 Vuex 的模块机制,并在实际项目中灵活运用。

模块化是大型应用开发的必备技能。 掌握了 Vuex 的模块机制,你就能更好地组织你的代码,提高代码的可维护性和可复用性。 记住,把你的 Vuex store 想象成一个乐高积木,每个模块都是一块积木,你可以根据需要自由组合,搭建出各种各样的应用。

好了,今天的讲座就到这里。 感谢大家的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注