美文网首页Vuex
Vuex源码解读

Vuex源码解读

作者: 宫若石 | 来源:发表于2019-07-12 16:12 被阅读0次

    导语

     随着应用复杂度的增加,我们需要考虑如何进行应用的状态管理,将业务逻辑与界面交互相剥离,Vue 为我们提供了方便的组件内状态管理的机制Vuex。本文将与实际场景应用相结合,分析我们在调用Vuex API,Vuex源码内是如何实现的。
    
    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
    

    1.Vuex核心思想

    Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
    Vuex 和单纯的全局对象有以下两点不同:
    
    1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

    2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是提交 (commit) mutation。

      通过下面demo,我将从Store注册、数据修改流程来介绍Vuex里是如何实现的。

    const ModuleA = {
      namespaced: true,
      state:{
        count: 1,
      },
      mutations:{
        increment(state){
          state.count++;
        }
      },
      actions:{
        increment(context){
          context.commit('increment');
        }
      },
      getters:{
        getNewCount(state){
          return state.count + 1
        }
      }
    }
    const ModuleB = {
      namespaced: true,
      state:{
        count: 1,
      },
      mutations:{
        increment(state){
          state.count++;
        }
      },
      actions:{
        increment(context){
          context.commit('increment');
        }
      },
      getters:{
        getNewCount(state){
          return state.count + 1
        }
      }
    }
    
    const store = new Vuex.Store({
      //定义每个module子仓库
      modules: {
        moduleA: ModuleA,
        moduleB: ModuleB
      }
    });
    

    2. Store初始化过程

    通常在使用Vuex时,会去调用new Vuex.Store方法,当前方法实现整个Store注册过程。Store主要执行分为ModuleCollection,installModule、resetStoreVM、注册插件步骤。
    
    image.png

    ModuleCollection主要功能将所有的modules建立父子关系,通过整个register逻辑,把整个关系建立起来,形成module树状结构。


    image.png

    ModuleCollection最终返回modules对象,如图:


    image.png
    installModule 方法对每个state、mutation、action、getter做了一层namespace。
    上述在代码中,我们定义ModuleA、ModuleB actions方法、state都是相同的,前面并没有加前缀,installModule方法就是在当前的加上namespace。这样的好处,互补影响。
    image.png

    makeLocalContext方法,遍历所有getters,然后给当前的getter加上namespace。这样就实现了在模块内调用getter里,通过defineProperty store.getters[type]就拿到了当前namespace的getter,这里的type就是拼接后的getter方法。makeLocalContext最终返回local对象,这个local在遍历Mutation、Action、Getter时使用。

    mutation实现,主要就是判断当前module有没有定义mutations,如果定义,循环mutations里的方法。通过namespace+key拼接,然后通过registerMutation方法实现注册。registerMutation会去把当前的方法push一个数组,最终返回wrappedMutationHandler方法,这个方法在我们执行commit方法的时候,就会执行handler.call方法,并最终返回当前local state。这就实现了我们在mutation方法中使用的state,实际上就是当前模块state。
    
    function registerMutation (store, type, handler, local) {
      var entry = store._mutations[type] || (store._mutations[type] = []);
      entry.push(function wrappedMutationHandler (payload) {
        handler.call(store, local.state, payload);
      });
    }
    

    action方法实现和mutation是类似的,action方法里会判断当前的action是不是root属性,如果是root就不做namespace拼接,然后在去执行registerAction方法,这里和registerMutation不同的是,他返回的对象里有当前模块的dispatch、commit、getters、state和root getters、root state。最后判断当前返回值有没有Promise,如果没有Promise,这里会把实际的返回转成Promise化。

    function registerAction (store, type, handler, local) {
      var entry = store._actions[type] || (store._actions[type] = []);
      entry.push(function wrappedActionHandler (payload, cb) {
        var res = handler.call(store, {
          dispatch: local.dispatch,
          commit: local.commit,
          getters: local.getters,
          state: local.state,
          rootGetters: store.getters,
          rootState: store.state
        }, payload, cb);
        if (!isPromise(res)) {
          res = Promise.resolve(res);
        }
      });
    }
    
     getter方法实现也是循环遍历当前模块的getter,然后做了一层namespace拼接,最后调用registerGetter方法。当我们调用getters里的方法就会调用wrappedGetter这个方法,并把当前模块的store传入,最终返回rawGetter方法,方法返回4个参数第一个参数当前模块state、第二个当前模块getters、第三个root state、第四个root getters。这个和上述方法不太一样,这里返回是参数不是对象。rawGetter方法实际上就是当前getters内定义的方法。
    
    function registerGetter (store, type, rawGetter, local) {
      store._wrappedGetters[type] = function wrappedGetter (store) {
        return rawGetter(
          local.state, // local state
          local.getters, // local getters
          store.state, // root state
          store.getters // root getters
        )
      };
    }
    
     installModule除了注册上述几个主要的步骤,他还做了针对children操作,判断当前的模块有没有children,如果有child再去调用installModule执行模块注册。
    
    image.png

    上图当执行完installModule方法后,store拿到的对象。

    resetStoreVM是主要做什么的?

     Vuex store提供在外层可以去调用state、getters方法,resetStoreVM就是实现在外层可以去调用state、getters功能;
    
    store.state.count;
    store.getters.getCount
    

    plugin

    plugin判断当前模块有没有传入plugins数组,如果有的话遍历plugins数组,然后调用plugin方法。
    

    3. 数据修改过程

     Vuex 本质上数据都存储在state下面,state修改只能通过mutations、actions去修改。actions与mutations的区别是,actions会执行一些异步操作,通常这种方式用于数据请求。下面介绍第一种commit方式:
    
    store.commit('increment')
    

    下面接下commit主要做了什么事情?

    Store.prototype.commit = function commit (_type, _payload, _options) {
        var this$1 = this;
    
      // check object-style commit
      var ref = unifyObjectStyle(_type, _payload, _options);
        var type = ref.type;
        var payload = ref.payload;
        var options = ref.options;
    
      var mutation = { type: type, payload: payload };
      var entry = this._mutations[type];
      this._withCommit(function () {
        entry.forEach(function commitIterator (handler) {
          handler(payload);
        });
      });
    };
    
     通过commit传入参数,拿到type。然后通过this._mutations[type]拿到wrappedMutationHandler方法,this._mututationsz在初始化state中有介绍过,然后在执行registerMutations里的handler方法,调用实际执行的mutation方法。
    

    第二种dispatch方式:

    store.dispatch('increment')
    

    对于dispatch是怎么实现的?

    Store.prototype.dispatch = function dispatch (_type, _payload) {
        var this$1 = this;
      // check object-style dispatch
      var ref = unifyObjectStyle(_type, _payload);
      var type = ref.type;
      var payload = ref.payload;
    
      var action = { type: type, payload: payload };
      var entry = this._actions[type];
      var result = entry.length > 1
        ? Promise.all(entry.map(function (handler) { return handler(payload); }))
        : entry[0](payload);
    };
    
     dispatch逻辑和commit逻辑比较相似,他也是通过this._actions[type]拿到当前action执行的方法,this._action在初始化state中有说。然后判断当前的entry有几个回调方法,如果回调方法大于1的话,执行Promise.all里所有方法;如果回调里方法个数小于等于1,就直接调用该方法。 
    

    4.组件绑定的辅助函数

     Store本身而言就是一个原生对象,他构造了一个数据仓库,他提供了一些常用的api修改这些数据仓库内容。在我们组件中也是可以同过store.state实现,但是这种方式我们使用组件中使用并不是太爽,所以vuex在这基础上又提供一些其他好用语法糖。
    
    import { mapState, mapGetters } from 'vuex'
    export default {
      name: 'App',
      computed:{
        ...mapState('moduleA',{
          beforeCount: 'count'
        }),
        ...mapGetters('moduleA',{
          afterCount: 'getCount'
        })
      }
    }
    
     在这里我们并没有通过store去读取state,而是通过mapState、mapGetters实现的读取state,先来分析mapState实现
    

    mapState实现

    var mapState = normalizeNamespace(function (namespace, states) {
      var res = {};
      normalizeMap(states).forEach(function (ref) {
        var key = ref.key;
        var val = ref.val;
    
        res[key] = function mappedState () {
          var state = this.$store.state;
          var getters = this.$store.getters;
          if (namespace) {
            var module = getModuleByNamespace(this.$store, 'mapState', namespace);
            if (!module) {
              return
            }
            state = module.context.state;
            getters = module.context.getters;
          }
          return typeof val === 'function'
            ? val.call(this, state, getters)
            : state[val]
        };
      });
      return res
    });
    
     mapSate支持两个参数,namespace和states两个参数。normalizeMap方法对传入states进行key、value转换,这样做的好处就是每个key都对应独立的返回。mappedState方法主要工作通过传入namespace调用getModuleByNameSpace拿到当前模块,然后返回对应模块的state。
    

    mapGetter实现

     mapGetter和mapState很像,首先对namespace进行一个拼接,拼接完之后,通过getModuleByNamespace查找,如果有的话,直接执行对应模块store.getters;如果没有,直接return。
    接下来介绍另外两个对于数据存储和修改方法。 
    
    import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
    
    export default {
      name: 'App',
      methods:{
        ...mapActions('moduleA',{
          getMapActions: 'increment'
        }),
        ...mapMutations('moduleA',{
          getMapMutations: 'increment'
        })
    }
    }
    

    mapMutation实现

     首先对当前传入mutations传做一层循环遍历,最终返回res[key]对象,在执行循环过程中调用getModuleByNamespace查找对应模块,如果没有,直接return。如果有的话,执行对应模块下的commit方法,最后判断mutations里方法是一个val还是function。并执行对应方法。
    
    var mapActions = normalizeNamespace(function (namespace, actions) {
      var res = {};
      normalizeMap(actions).forEach(function (ref) {
        var key = ref.key;
        var val = ref.val;
    
        res[key] = function mappedAction () {
          var args = [], len = arguments.length;
          while ( len-- ) args[ len ] = arguments[ len ];
    
          // get dispatch function from store
          var dispatch = this.$store.dispatch;
          if (namespace) {
            var module = getModuleByNamespace(this.$store, 'mapActions', namespace);
            if (!module) {
              return
            }
            dispatch = module.context.dispatch;
          }
          return typeof val === 'function'
            ? val.apply(this, [dispatch].concat(args))
            : dispatch.apply(this.$store, [val].concat(args))
        };
      });
      return res
    });
    

    mapAction实现

     对于actions和mutation方法是类似的,只是区别在于这里执行的是dispatch方法。
     辅助函数是在原有的基础上,实现以最少的代码实现原有的方法,简化开发者使用。
    

    5.动态更新模块

    Vuex除了提供辅助函数,还提供动态加载新模块。调用方法如下:

    this.$store.registerModule('moduleC', {
      namespaced: true,
      state: {
        count: 1,
      },
      mutations:{
        increment(state){
          state.count++;
        }
      },
      actions:{
        increment(context){
          context.commit('increment');
        }
      },
      getters:{
        getCount(state){
          return state.count + 1
        }
      }
    })
    

    最终我们拿到的store,如下:


    image.png

    上述的代码在Vuex主要实现:

    function registerModule (path, rawModule, options) {
      if ( options === void 0 ) options = {};
      if (typeof path === 'string') { path = [path]; }
      this._modules.register(path, rawModule);
      installModule(this, this.state, path, this._modules.get(path), options.preserveState);
      // reset store to update getters...
      resetStoreVM(this, this.state);
    };
    
    registerModule检测当前的path是不是string,检测完了之后,就会调用当前module里的register方法,然后再调用installModule、resetStoreVM实现新的module注册。installModule、resetStoreVM在初始化state中介绍,这里就不做解释。
    

    Vuex提供注册方法,就有注销方法——unregisterModule方法。

    function unregisterModule (path) {
      var this$1 = this;
      if (typeof path === 'string') { path = [path]; }
    
      this._modules.unregister(path);
      this._withCommit(function () {
        var parentState = getNestedState(this$1.state, path.slice(0, -1));
        Vue.delete(parentState, path[path.length - 1]);
      });
      resetStore(this);
    };
    
    unregisterModule方法,通过传入path,执行this.modules的unregister方法,然后最终会把数据做一次恢复。最后再执行resetStore。resetStore方法会对actions、mutations方法做一次清空。清空完之后,再重新执行一次installModule、resetStoreModule方法。
    

    6.总结

    通常我们开发时通过传参的方法对于多层嵌套的组件将会非常麻烦,Vuex解决了组件之间共享同一状态的麻烦问题。其实Vuex还提供一些额外辅助方法,尤其是mapXXX的设计,使程序更加简洁,有更高的可读性。我们在使用API的时候更加方便。这也是我们今后在设计一些JS库的时候,从API的角度中应该学习的方向
    

    相关文章

      网友评论

        本文标题:Vuex源码解读

        本文链接:https://www.haomeiwen.com/subject/teqykctx.html