前言
这篇文章是上一偏文章的进阶,上一篇我们实现了vuex的基本功能,接下来将实现vuex的模块化、命名空间等。
分析
在大型项目中,我们通常会将vuex进行模块化,否者随着项目迭代,代码必将越来越臃肿。模块化实际上就是将我们的store对象进行分割成一个个模块,每个模块都有自身的state 、mutations、actions、getters、module。
模块化有一个问题,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。于是我们需要用到namespaced命名空间,所有 getter、action 及 mutation 都将会自动根据模块注册的路径调整命名。
实现
将所有模块生成一棵树
class Store{
constructor(options){
// 格式化用户传入的参数
// 1、收集模块转换成一棵树
this._modules = new ModuleCollection(options);
...
}
...
}
export default class ModuleCollection{
constructor(options){
// 注册模块 递归注册
this.register([], options);
}
register(path, rootModule){
let newModule = new Module(rootModule);
if(path.length == 0){
this.root = newModule;
}else{
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current);
}, this.root);
parent.addChild(path[path.length-1], newModule);
}
// 如果有modules说明有子模块
if(rootModule.modules) {
forEach(rootModule.modules, (module, moduleName) => {
this.register([...path, moduleName], module);
})
}
}
...
}
export default class Module{
constructor(rootModule){
this._rawModule = rootModule;
this._children = {};
this.state = rootModule.state;
}
getChild(key){
return this._children[key];
}
addChild(key, module){
this._children[key] = module;
}
...
}
通过递归,对用传入的选项生成了一棵新的树,将state放在每个模块下,方便后续使用命名空间时,保证是访问当前模块的state。
安装模块
class Store{
constructor(options){
// 2、安装模块 将模块上的属性 定义在我们store中
let state = this._modules.root.state;
this._mutations = {}; // 存放所有模块中的mutations
this._actions = {}; // 存放所有模块中的actions
this._wrappedGetters = {}; // 存放所有模块中的getters
this._subscribers = [];
installModule(this, state, [], this._modules.root);
...
}
...
}
function installModule(store, rootState, path, module){
// 注册事件时 需要注册到对应的命名空间中 path就是所有空间 根据path算出空间来
let namespace = store._modules.getNamespace(path);
if(path.length > 0){ // 如果是子模块,需要将子模块的状态定义到根模块上
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current];
}, rootState);
Vue.set(parent, path[path.length - 1], module.state)
}
module.forEachMutation((mutation, type) => {
store._mutations[namespace + type] ||= [];
store._mutations[namespace + type].push((payload) => {
mutation.call(store, module.state, payload)
})
});
module.forEachAction((action, type) => {
store._actions[namespace + type] ||= [];
store._actions[namespace + type].push((payload) => {
action.call(store, store, payload)
})
});
module.forEachGetter((getter, key) => {
store._wrappedGetters[namespace + key] = function(params){
return getter(module.state);
}
});
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child)
});
}
export default class ModuleCollection{
...
// 获取命名空间
getNamespace(path){
let root = this.root;
return path.reduce((namespaced, key) => {
root = root.getChild(key);
// 如果模块的namespaced属性为true就会返回路径
return namespaced + (root.namespaced ? key + "/" : "");
}, "")
}
}
将状态放到vue实例上
class Store{
constructor(options){
...
resetStoreVm(this, state);
...
}
// 类的属性访问器,当用户去这个实例上访问state属性时会触发这个方法
get state(){
return this._vm._data.$$state;
}
...
}
function resetStoreVm(store, state){
const wrappedGetters = store._wrappedGetters;
let computed = {};
store.getters = {};
forEach(wrappedGetters, (fn, key) => {
computed[key] = function(){
return fn();
}
Object.defineProperty(store.getters, key, {
get(){
return store._vm[key]
},
})
})
store._vm = new Vue({
data: {
$$state: state,
},
computed,
})
}
commit和dispatch
class Store{
constructor(options){
...
}
commit = (type, payload) => {
this._mutations[type].forEach(fn => fn(payload));
}
dispatch = (type, payload) => {
this._actions[type].forEach(fn => fn(payload));
}
...
}
mapState
export const mapState = (arrList) => {
let obj = {};
for(let i=0; i< arrList.length; i++){
let stateName = arrList[i];
obj[stateName] = function(){
return this.$store.state[stateName];
}
}
return obj;
}
mapState、mapGetter实现原理都差不多,这里就写一个mapState好了。实际上都是接受一个数组作为参数返回一个对象,对象的key是state属性名,value是一个函数,因为是要在computed计算属性里面解构使用的。
使用
const store = new Vuex.Store({
state: {
age: 18,
},
mutations: {
changeAge(state, payload) {
state.age += payload;
}
},
actions: {
asyncChangeAge(store, payload) {
setTimeout(() => {
store.commit("changeAge", payload);
}, 1000);
}
},
modules: {
index: {
namespaced: true,
state: {
num: 1,
age:10,
},
mutations: {
changeNum(state) {
state.num++;
},
changeAge(state) {
state.age++;
},
},
getters: {
otherAge(state) {
return state.age + 10;
}
},
modules: {
a: {
namespaced: true,
state: {
num: 2,
}
}
}
}
},
getters: {
otherAge(state) {
return state.age + 10;
}
}
})
export default store;
new Vue({
store,
render: h => h(App)
}).$mount('#app')
<template>
<div id="app">
<p>我的年龄:{{ age }}</p>
<p>她的年龄:{{ $store.getters.otherAge }}</p>
<button @click="$store.commit('changeAge', 1)">同步修改</button>
<button @click="$store.dispatch('asyncChangeAge', 2)">异步修改</button>
<p>{{ $store.state.index.age }}</p>
<button @click="$store.commit('index/changeAge')">index模块</button>
</div>
</template>
<script>
import {mapState} from './vuex'
export default {
name: "App",
created() {
console.log(this.$store);
},
computed: {
...mapState(['age'])
},
};
</script>
有兴趣的朋友可以继续查看完整项目代码,代码地址:vue-vuex-mini。