菜鸡手写vuex(二)

148 阅读3分钟

前言

这篇文章是上一偏文章的进阶,上一篇我们实现了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>

vuex-show.gif 有兴趣的朋友可以继续查看完整项目代码,代码地址:vue-vuex-mini

菜鸡手写vuex(一)
菜鸡手写vuex(二)