深入理解 Vuex 源码中 `Module` (模块) 的注册和命名空间 (namespaced) 实现。

各位观众老爷们,晚上好!我是老码,今天咱们聊聊 Vuex 源码里那些弯弯绕绕,特别是关于 Module 的注册和 namespaced 的实现。这部分内容,说白了,就是把你的 Vuex store 划分成一个个小格子,每个格子都有自己的地盘,互不干扰。

准备好了吗?咱们这就开始!

第一章:从 Store 的初始化说起:installModule 的调用

要理解 Module 的注册,必须先看看 Store 是怎么初始化的。在 Vuex 的核心代码里,Store 的构造函数会调用一个叫做 installModule 的方法,这个方法就是负责把你的模块一个一个注册到 Vuex store 里的。

// 简化后的 Store 构造函数
class Store {
  constructor (options = {}) {
    // ... 省略其他初始化代码 ...

    // 根模块
    this._modules = new ModuleCollection(options);

    // ... 省略其他代码 ...

    // 安装模块
    installModule(this, this.state, [], this._modules.root);

    // ... 省略其他代码 ...
  }
}

看到了吗?installModule 被调用了!它接收四个参数:

  • this: 指向当前的 Store 实例。
  • this.state: 指向 Vuex store 的根状态。
  • []: 一个空的路径数组,表示当前正在安装根模块。
  • this._modules.root: 指向根模块的 Module 实例。

ModuleCollection 是干什么的?简单来说,它负责把你的 Vuex options 里的 modules 选项,转换成一个树状结构,方便后续的模块注册。

第二章:installModule 的真面目:模块注册的核心逻辑

installModule 是个递归函数,它会遍历模块树,把每个模块都注册到 Vuex store 里。咱们来看看它的简化版代码:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length; // 是否是根模块
  const namespaced = store._modules.getNamespace(path); // 获取命名空间

  // 1. 注册 mutations, actions, getters
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)); // 获取父模块的状态

    // 注册子模块的状态
    Vue.set(parentState, path[path.length - 1], module.state);
  }

  // 注册 mutations
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespaced + key;
    registerMutation(store, namespacedType, mutation, module);
  });

  // 注册 actions
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespaced + key; // root action 不需要命名空间
    const handler = action.handler || action;
    registerAction(store, type, handler, module);
  });

  // 注册 getters
  module.forEachGetter((getter, key) => {
    const namespacedType = namespaced + key;
    registerGetter(store, namespacedType, getter, module);
  });

  // 2. 递归安装子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot);
  });

  // 3. 注册 watch
  if (module.namespaced) {
    store._devtoolHook && store._devtoolHook.emit('vuex:module:add', path, module);
  }
}

这段代码看起来有点长,但其实逻辑很清晰:

  1. 注册 mutations, actions, getters: 遍历模块的 mutations, actions, getters,然后调用 registerMutation, registerAction, registerGetter 函数,把它们注册到 Vuex store 的内部状态里。注意,这里会根据 namespaced 的值,给 mutations, actions, getters 加上命名空间。
  2. 递归安装子模块: 遍历模块的子模块,然后递归调用 installModule 函数,把子模块也注册到 Vuex store 里。
  3. 注册 watch: 如果模块是命名空间的,就通知 Vue Devtools,方便调试。

第三章:命名空间的奥秘:namespaced 属性的作用

namespaced 属性是 Vuex 模块里一个很重要的属性。它决定了模块的 mutations, actions, getters 是否要加上命名空间。

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

如果 namespacedtrue,那么:

  • mutation increment 的类型会变成 moduleA/increment
  • action increment 的类型会变成 moduleA/increment
  • getter doubleCount 的访问路径会变成 moduleA/doubleCount

这样做的好处是:

  • 避免命名冲突: 不同的模块可以使用相同的 mutation, action, getter 名称,而不会发生冲突。
  • 提高代码可维护性: 模块之间的依赖关系更加清晰,代码更容易理解和维护。

第四章:registerMutation, registerAction, registerGetter 的实现细节

这三个函数负责把 mutations, actions, getters 注册到 Vuex store 的内部状态里。咱们来看看它们的简化版代码:

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []);
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(local, store.state, payload)
  })
}

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(local, {
      dispatch: store.dispatch,
      commit: store.commit,
      getters: store.getters,
      state: store.state,
      rootState: store.state
    }, payload)
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    return res
  })
}

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    console.error(`[vuex] duplicate getter key: ${type}`)
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (storeState, storeGetters) {
    return rawGetter.call(local,
      local.state, // local state
      local.getters, // local getters
      storeState, // root state
      storeGetters // root getters
    )
  }
}

这三个函数做的事情很简单:

  • registerMutation: 把 mutation handler 包装成一个函数,然后添加到 store._mutations 对象里。
  • registerAction: 把 action handler 包装成一个函数,然后添加到 store._actions 对象里。Action 包装器中,向 action 注入 dispatch, commit, getters, state, rootState 等上下文对象,方便在 action 中进行状态修改和派发其他 action。
  • registerGetter: 把 getter 函数包装成一个函数,然后添加到 store._wrappedGetters 对象里。 Getter 包装器中,处理了局部 state/getters 和全局 state/getters 的注入,使得 getter 可以访问到不同层级的数据。

第五章:ModuleCollection 的作用:构建模块树

ModuleCollection 负责把你的 Vuex options 里的 modules 选项,转换成一个树状结构。咱们来看看它的简化版代码:

class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  register (path, rawModule, runtime) {
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
}

这段代码主要做了三件事:

  1. register: 注册模块。它会创建一个 Module 实例,然后把它添加到模块树里。
  2. getNamespace: 获取模块的命名空间。它会遍历模块树,把每个模块的命名空间拼接起来。
  3. get: 根据路径获取模块。

第六章:Module 类的实现:存储模块的信息

Module 类负责存储模块的信息,比如 state, mutations, actions, getters, children 等。咱们来看看它的简化版代码:

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) || {}
    this.namespaced = !!rawModule.namespaced

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

    forEachValue(rawModule.mutations, (mutation, key) => {
      this._mutations[key] = mutation
    })

    forEachValue(rawModule.actions, (action, key) => {
      this._actions[key] = action
    })

    forEachValue(rawModule.getters, (getter, key) => {
      this._getters[key] = getter
    })
  }

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

  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)
  }
}

这段代码主要做了这些事情:

  • 构造函数: 初始化模块的 state, mutations, actions, getters, children 等属性。
  • getChild, addChild: 用于访问和添加子模块。
  • forEachMutation, forEachAction, forEachGetter, forEachChild: 用于遍历模块的 mutations, actions, getters, children。

第七章:总结:模块注册和命名空间的流程

咱们来总结一下 Vuex 模块注册和命名空间的流程:

  1. Store 构造函数调用 installModule 函数,开始注册模块。
  2. installModule 函数会递归遍历模块树,把每个模块都注册到 Vuex store 里。
  3. 对于每个模块,installModule 函数会:
    • 注册 mutations, actions, getters,并根据 namespaced 属性,给它们加上命名空间。
    • 递归安装子模块。
  4. ModuleCollection 负责把 Vuex options 里的 modules 选项,转换成一个树状结构,方便后续的模块注册。
  5. Module 类负责存储模块的信息,比如 state, mutations, actions, getters, children 等。

为了更直观地理解,咱们用一个表格来总结一下:

| 步骤 | 负责的函数/类 | 主要作用

| Store 构造函数 | 调用 installModule 函数,启动模块注册流程。

发表回复

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