各位听众,大家好!今天咱们来聊聊 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
: 存储用户定义的原始模块对象,就是你写的state
、mutations
、actions
、getters
这些东西。_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) //递归调用
})
}
这个函数做了几件事:
- 计算命名空间: 根据模块的路径(
path
)和namespaced
属性,计算出模块的命名空间。 - 注册状态: 把模块的 state 注册到 Vuex 的根 state 上,如果是子模块,就注册到父模块的 state 上。
- 注册 mutations、actions、getters: 遍历模块的 mutations、actions、getters,把它们注册到 Vuex 的
_mutations
、_actions
、_getters
对象上。 - 递归注册子模块: 如果模块有子模块,就递归调用
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
函数会这样计算命名空间:
moduleA
启用了命名空间,所以命名空间字符串变成'moduleA/'
。moduleB
没有启用命名空间,所以命名空间字符串不变。moduleC
启用了命名空间,所以命名空间字符串变成'moduleA/moduleC/'
。
所以,moduleC
的命名空间就是 'moduleA/moduleC/'
。 Vuex 在注册 moduleC
的 mutations、actions、getters 的时候,都会加上这个前缀。
六、mapState
、mapGetters
、mapMutations
、mapActions
:让你的代码更简洁
手动拼接命名空间太麻烦了,Vuex 提供了 mapState
、mapGetters
、mapMutations
、mapActions
这几个辅助函数,可以让你更方便地访问模块的状态、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 想象成一个乐高积木,每个模块都是一块积木,你可以根据需要自由组合,搭建出各种各样的应用。
好了,今天的讲座就到这里。 感谢大家的聆听!