Vuex源码解读

导语

 随着应用复杂度的增加,我们需要考虑如何进行应用的状态管理,将业务逻辑与界面交互相剥离,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的角度中应该学习的方向

推荐阅读更多精彩内容

  • 前言 之前几篇解析 Vue 源码的文章都是完整的分析整个源码的执行过程,这篇文章我会将重点放在核心原理的解析,不会...
    心_c2a2阅读 502评论 1 8
  • Vuex源码阅读分析 Vuex是专为Vue开发的统一状态管理工具。当我们的项目不是很复杂时,一些交互可以通过全局事...
    steinslin阅读 131评论 0 4
  • ### store 1. Vue 组件中获得 Vuex 状态 ```js //方式一 全局引入单例类 // 创建一...
    芸豆_6a86阅读 213评论 0 3
  • ### store 1. Vue 组件中获得 Vuex 状态 ```js //方式一 全局引入单例类 // 创建一...
    芸豆_6a86阅读 41评论 0 0
  • 安装 npm npm install vuex --save 在一个模块化的打包系统中,您必须显式地通过Vue.u...
    萧玄辞阅读 1,673评论 0 6