一文弄懂vuex源码

前言

vuex作为一款强有力的状态管理工具被广泛应用于实际工作当中,通过学习vuex的源码可以帮助我们解决藏于心中很久的困惑.

比如vuex的全局状态存放到了哪个地方?为什么修改store里面的状态,页面也会同步更新?actionmutation它们是如何协作修改状态的?为什么action里面建议写异步操作,而mutation定义成同步?

很多实际使用过vuex的同学相信心中会存在这些疑问.本文将摒弃从头到尾直述源码的方式,从提出问题再结合场景去研究源码的实现过程,从而将上述疑问一一解答.

核心原理

首先从框架的思维跳出来,我们先实现一个简易版的vuex,直接窥探它如何做到响应式控制页面变化的.

观察下面代码,起始先定义一个Vue实例vm,注意这个vm它只定义了一个data属性,它没有和页面上的dom节点相绑定,也没有创建其他的属性和方法.

Store是一个构造函数,用来创建store对象,它会将vm赋予store对象.

Object.definePropertiesStore构造函数的原型上定义了一个state,并且设置了state的获取函数.

    const vm = new Vue({
      data:{
        value:"hello world"
      }
    })

    function Store(){
        this._vm = vm;
    }

    Object.defineProperties(Store.prototype,{
      state:{
          configurable:true,
          get(){
              return this._vm;
          }
      }  
    })
    
    const store = new Store(); // 创建一个store对象     

从上面代码可以看出,使用Store构造函数创建的store对象使用了一层代理.比如store.state.value会直接从get函数中获取值,而get函数又是从vm中拿值.那么从store.state.value中拿到的值其实是从vm定义的data中拿到的.

vm是一个Vue实例,它定义的状态具有响应式特性,这也就间接使store.state具有响应式.

store对象已经准备好了,接下来要将它注入到页面当中使用.Vue.mixin可以轻松做到这一点(代码如下).

它会在每一个Vue实例创建的过程中添加created生命周期,函数内通过this.$options拿到配置项判端是否传递了store属性,如果包含就赋值给this.$store.

这样在Vue实例内部通过this就可以拿到store对象了.然后我们开始开发页面,使用new新构建一个Vue实例app.

app会被挂载到页面节点#app中,store作为参数赋值,另外计算属性里面定义了一个属性value.

 Vue.mixin({
        created() {
            if(this.$options.store){
                this.$store = this.$options.store;
            }
        }
 })

 const app = new Vue({
        el: '#app',
        store,
        computed:{
            value(){
                return this.$store.state.value;
            }
        }
 })

此时页面的模板里填写{{value}}会发现网页渲染出了hello world.我们会惊奇的发现vm定义的状态最终会映射到app的计算属性里.

数据的投射只是一方面,如果此时修改vm.value = hello,页面会重新渲染,内容变成hello.

讲述到这里,vuex的响应式原理已经逐渐清晰.vmapp都是构建出来的Vue实例.

vmdata相当于一个数据仓库,而app会使用仓库的数据.由于vmdata具有响应式,所以对vm的修改也会触发app模板的重新渲染.

上面的场景过于简单,它的数据结构仅仅只是一个对象.在实际开发的需求里,页面的状态要复杂的多,我们通常会将store的状态划分到不同模块中处理.

vm直接修改状态虽然简单,但直接修改太过暴力并且错误不容易追踪,所以也就衍生出了actionmutation来修改状态的方式.

我们接下来深入研究一下vuex如何支持模块化的数据以及状态的修改.

模块化实现

vuex源码定义的Store构造函数如下,首先定义了大量的初始化属性,其中this._modules = new ModuleCollection(options)这一句很关键.

options是开发者定义的配置项,将其传入ModuleCollection生成一个根模块_modules.

紧接着执行了两个很重要的方法.一个是installModule安装模块,另一个是resetStoreVM将状态响应化处理.

var Store = function Store (options) {
  var this$1 = this;
  ...
  this._committing = false;
  this._actions = Object.create(null);
  this._actionSubscribers = [];
  this._mutations = Object.create(null);
  this._wrappedGetters = Object.create(null);
  this._modules = new ModuleCollection(options); //构建模块
  this._modulesNamespaceMap = Object.create(null);
  this._subscribers = [];
  this._watcherVM = new Vue();

  // bind commit and dispatch to self
  var store = this;

  var state = this._modules.root.state;
   
  //安装模块 
  installModule(this, state, [], this._modules.root);
  
  //状态响应化
  resetStoreVM(this, state);

  ...
};

构建模块化对象

new ModuleCollection(options)会构建一个模块对象赋值给this._modules(这个this指向store实例对象).

我们先回顾一下开发者定义的options的模块化配置.

// 导出一个store对象
export default new Vuex.Store({
  state,
  mutations,
  actions,
  modules: {
    home, // home模块
    login // login模块
  }
})

在全局下可以配置state,mutations以及actions,在每一个子模块下也能配置state,mutations以及actions.

这里之所以要构建模块对象返回,主要原因是因为开发者定义的配置对象不利于操作和计算,所以要将原始的配置对象转化成另外一种更加方便使用的数据结构.

ModuleCollectionoptions处理代码如下,它最终会返回一个模块对象.首次调用先执行register注册根模块,此时path是一个空数组.

new Module直接初始化了一个实例对象newModule,它主要包含三个属性:_children,_rawModulestate.

  • _children:当前模块的子模块
  • _rawModule:当前模块的配置项(开发者定义的option)
  • state:当前模块的数据状态

register函数是核心方法.它会根据配置项构造出模块对象newModule,然后根据path数组的长度来判断当前模块是不是根模块.

如果是根模块,就把newModule赋值给root属性.如果不是根模块,它就会通过path获取当前模块的父模块,再将当前模块赋予父模块的_children上.

再往下判断rawModule.modules是否存在,如果发现还有子模块的配置,继续递归调用register函数.

var ModuleCollection = function ModuleCollection (rawRootModule) {
  // rawRootModule对应着options
  this.register([], rawRootModule, false);
};


ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
  var newModule = new Module(rawModule, runtime);
  if (path.length === 0) {
    this.root = newModule;
  } else {
    var parent = this.get(path.slice(0, -1));//根据path获取父模块
    parent.addChild(path[path.length - 1], newModule);//将子模块赋予父模块的``_children``上.
  }

  // register nested modules
  if (rawModule.modules) {//发现当前配置存在子模块的配置
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      //拿出每一个子模块的配置项,递归调用register
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};

Module.prototype.addChild = function addChild (key, module) {
  this._children[key] = module;
};

//每个模块初始化了_children,_rawModule,state三个属性,state的值是从配置项中获取
var Module = function Module (rawModule, runtime) {
  this.runtime = runtime;
  this._children = Object.create(null);
  this._rawModule = rawModule;
  var rawState = rawModule.state;
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {};
};

上面这一轮的操作最终的目的就是为了返回一个数据对象,数据结构如下.该条数据包含了父子级的层级结构,每一级都拥有自己的state状态.

{
 root:{
     {
        state: {userInfo: {…}},
        _children:{
            home: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}}
            login: Module {runtime: false, _children: {…}, _rawModule: {…}, state: {…}}
        },
        _rawModule: {state: {…}, mutations: {…}, actions: {…}, modules: {…}},
        namespaced: false
     }
 }
}

返回的数据对象会赋值给this._modules(这个this指向store实例).

模块化处理

this._modules的数据构建完毕,从根模块里取出初始状态state并联合其他参数传入installModule函数执行模块的安装(代码如下).

var Store = function Store (options) {
   ...
  
  this._modules = new ModuleCollection(options);
  
   ...
  
  var state = this._modules.root.state;

  installModule(this, state, [], this._modules.root);
  
  ...
}

installModule源码如下.模块的安装过程可以分为以下四步.

  • 设置状态的层级结构.以根模块的rootState为初始值,每个子模块以模块名为key,以子模块的数据状态为value赋值到父模块的状态对象上.例如生成的数据结构如下.
store._module = {
  root:{
     state:{
        user_info:null,//user_info是根模块的状态信息
        home:{  //list是home模块(子模块)的状态,以子模块的名称为``key``,状态为值赋予父模块的状态对象上
          list:[]
        }
     }
  }
}
  • 构建当前模块的本地上下文对象.makeLocalContext函数返回一个local对象,里面包含dispatch,commit,gettersstate.返回的local对象赋予模块对象的context.

  • 以上面local对象为基础,开始全局注册actions,mutationsgetters.

  • 前三步完成代表当前模块安装完毕.如果发现当前模块含有子模块,则继续递归调用installModule安装每个子模块.

function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length;//path为空数组时,说明是根模块
  //namespace的形式,如果是根模块为空字符串"",有一个home子模块格式为"home/"
  var namespace = store._modules.getNamespace(path);

  // 使用键值对将module存储起来
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module;
  }

  //设置状态的层级结构,根模块不执行
  if (!isRoot && !hot) {
    //获取父级的state
    var parentState = getNestedState(rootState, path.slice(0, -1));
    //获取当前模块的name
    var moduleName = path[path.length - 1];
    store._withCommit(function () {
      //将当前模块的状态赋值给父级模块的状态对象上  
      Vue.set(parentState, moduleName, module.state);
    });
  }
  
  //构造了当前模块的本地上下文对象,里面包含dispatch,commit,getters和state
  //它们都只操作当前模块的内容,local被赋予module.context  
  var local = module.context = makeLocalContext(store, namespace, path);
    
  //开始注册actions,mutations和getters 
  module.forEachMutation(function (mutation, key) {
    var namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
  });

  module.forEachAction(function (action, key) {
    var type = action.root ? key : namespace + key;
    var handler = action.handler || action;
    registerAction(store, type, handler, local);
  });

  module.forEachGetter(function (getter, key) {
    var namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
  });
  
  //递归安装子模块,forEachChild内部遍历module._children拿到每个子模块
  module.forEachChild(function (child, key) {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}

现在看一下local对象的组成以及actions,mutationsgetters的注册过程.

local对象是运行makeLocalContext(store, namespace, path)返回的结果,它最终会被存储在module.context下面.

makeLocalContext函数体内定义了一个变量名为local的对象,里面包含dispatchcommit两个函数,另外还对localgettersstate做了get配置,最终返回local对象.

从整体上看,local对象对外暴露了四个属性分别是dispatch,commit,gettersstate.这四个属性只服务于当前模块.

比如开发者在根模块下配置了一个home模块,那么home模块就会拥有一个属于自己的local对象.此local对象执行dispatchcommit就会只执行home模块内定义的actionsmutations的函数.另外gettersstate也指向了home模块内的getters函数和state.

local四个属性的实现后面再讲.主流程获取local对象后开始注册actions,mutationsgetters.

先看下面actions的注册过程(代码如下),module.forEachAction拿到的action就是开发者在该模块下定义的action函数.

storevuex最终返回的实例对象,type对应着action的函数名,如果是在子模块里,type为模块名与函数名拼接的结果,比如home模块下actions定义了一个getList(...){...},对应的type就为home/getList.

registerAction函数正式开始注册actions,上面讲解Store构造函数已经提及了this._actions = Object.create(null)的初始化定义.registerAction函数目的就是在store对象下的_actions里塞进去一个处理函数,对象的key值便是type.

module.forEachAction(function (action, key) {
    var type = namespace + key;
    var handler =  action;
    registerAction(store, type, handler, local);
});

function registerAction (store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload, cb) {
     ...
  });
}

最后执行的结果便是下面这样.将各个模块下定义的actions函数全部打平放到store对象下的_actions里面.

store = {
   ...
    _actions:{
        home/getList: [ƒ],  //home模块下的getList函数
        home/getNav: [ƒ], // home模块下的getNav函数
        getUser: [ƒ] //根模块的getUser函数
    }
    ...
}

我们再看一下塞到全局的actions函数wrappedActionHandler内部的具体实现.它的内部利用闭包关联了全局store对象、当前模块的local对象以及开发者定义的actions函数handler.

wrappedActionHandler函数内部直接执行handler,但是给actions函数传递的dispatch,commit,gettersstate都是从local中获取的.这就是为什么开发者在定义模块下的actions函数时,函数的参数dispatch,commit,gettersstate只会作用于当前的模块.

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);
    }
    return res    
  });

举个例子.比如home模块下的定义了一个getList函数,函数的参数state它指向home模块下的state,而commit则会调用home模块下mutations定义的setList函数,而不会从全局范围内去找.

 actions:{ //在action中可以进行ajax请求数据并对数据进行处理
    getList({state,commit}){
        ajax({id:state.id}).then((data)=>{
            commit("setList",data);
        }) 
    }
}

上述handler返回的结果会做Promise化处理,这就意味着wrappedActionHandler返回的结果一定是一个Promise.

action的注册已经介绍完毕,说到底就是将各个模块定义的actions函数全部提取出来做一层封装处理塞到全局store对象下的_actions属性里面.

我们再来看看mutationsgetters的注册.它们做的处理方式和actions类似,各个模块定义的mutations函数全部提取出来做一层封装处理塞到全局store对象下的_mutations中,而getter则塞到store对象下的_wrappedGetters里.

module.forEachMutation(function (mutation, key) {
    var namespacedType = namespace + key;
    registerMutation(store, namespacedType, mutation, local);
});

module.forEachGetter(function (getter, key) {
    var namespacedType = namespace + key;
    registerGetter(store, namespacedType, getter, local);
});

//注册mutation
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);
  });
}

//注册getter
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  };
}

最终执行完actions,mutationsgetters的注册,store对象的数据结构变成了如下形式.

store = {
    _actions:{
        home/getList: [ƒ],  //home模块下actions中定义的getList函数
        home/getNav: [ƒ], // home模块下actions中定义的getNav函数
        getUser: [ƒ] //根模块下actions中定义的getUser函数
    },
    _mutations:{
        logout: [ƒ], // 根模块下mutations中定义的logout函数 
        home/setList: [ƒ], // home模块下mutations中定义的setList函数 
        home/setNav: [ƒ] //  home模块下mutations中定义的setNav函数 
    },
    _wrappedGetters:{
        home/getFilterList: ƒ wrappedGetter(store) // home模块下getters中定义的函数 
    }  
}

上面花了这么大的力气就是为了将store的数据结构改造成这种形式,这种形式到底能发挥出什么作用呢?

熟悉vuex的同学都知道,调用actions函数要使用dispatch,调用mutations函数要使用commit.我们现在来看一下store对象下的dispatchcommit如何定义.

在页面组件内,当我们使用this.$store.dispatch(type,payload)就能触发某个action函数的执行.从调用方式来看,通常dispatch会传递两个参数,第一个是action的函数名,第二个是参数.commit的执行方式也类似.

先看dispatch的源码(代码如下),调用dispatch时通常会传入typepayload.Store.prototype.dispatch里面有句关键代码var entry = this._actions[type].通过typestore对象下的_actions寻找处理函数,再将payload传入执行该函数.

看到这里就已经明白了为什么store下面要构建_actions,_mutations_wrappedGetters数据结构.

那是因为dispatchcommit它们要做的就是去_actions,_mutations寻找处理函数并执行.比如dispatch('getUser')就是执行根模块下的getUser.如果typehome/getList,那么dispatch执行就是home模块下的getList.

dispatch成为了统一的调用入口,而只要改变type的参数形式,就能让dispatch调用到任意模块下的action,如此便实现了dispatch函数对模块化的支持.commit实现原理类似,只不过它是从_mutations里面获取处理函数.

var Store = function Store (options) {
 ...
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
  };
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
  };
 ...
}

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);

  return result.then(function (res) {
    return res
  })
};

Store.prototype.commit = function commit (_type, _payload, _options) {
  var this$1 = this;
  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);
    });
  });
};

现在我们来梳理一下整个执行流程,页面组件首先调用this.$store.dispatch("home/getList"),此时Store.prototype.dispatch函数就会响应从store._actions下面拿到home/getList对应的处理函数.

而这个处理函数是在registerAction函数里面生成的,对应着下面代码中的wrappedActionHandler.这个函数里面的handler正是开发者定义的actions里的函数,执行handler时修改它的上下文环境并传入该模块下的local对象里的属性作为参数.

handler是开发者定义的actions里的函数,函数内业务逻辑执行完毕后通常会执行commit操作调用mutation.函数内的commit正是下面的local.commit(后面会将),local.commit底层调用是全局store对象的commit,它又会根据typestore._mutations下面寻找处理函数.

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);
    }
    return res    
  });

store._mutations对应的处理函数上面已经讲过,它是在registerMutation函数内生成的.对应着下面代码中的wrappedMutationHandler函数.函数内的handler同样对应着开发者定义的mutations里的函数,将local.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);
  });
}

一般mutations函数里面会直接操作state,一旦改变了state的数据,页面就会重现渲染.整个流程走下来,我们便理解了actionsmutations只是一种改变数据的机制.

开发者一般会在actions里面定义异步逻辑获取数据,在mutations里面定义同步逻辑操作数据,这样机制能让修改数据的整个过程容易记录和追踪,从而增强了程序的健壮性.

action写异步,mutation写同步

现在回到文章的开头,为什么action里面建议写异步操作,而mutation定义成同步?

我们再看一下dispatchcommit的定义.在dispatch函数里面,通过this._actions[type]拿到了处理函数数组entry,接下来通过Promise.all执行数组内的handler,将返回值赋予result.

在上面讲到的生成action函数的wrappedActionHandler里面是将返回值Promise化的,换句话说entry里面的handler的返回值一定是一个Promise,所以这边能使用Promise.all来调用,返回的result仍然是一个Promise,result再调用then返回res.

Store.prototype.dispatch = function dispatch (_type, _payload) {
  var this$1 = this;
  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);

  return result.then(function (res) {
    return res
  })
};

Store.prototype.commit = function commit (_type, _payload, _options) {
  var this$1 = this;
  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);
    });
  });
};

由此可见上面dispatch最终返回的是一个Promise,那么在页面上(代码如下)调用dispatch就可以使用then来监听action函数的异步操作有没有执行完毕(代码如下).正是因为dispatch里面多了一层Promise处理,所以让异步操作变得容易监听和追踪,这就十分利于其他调试工具的记录.

我们再观察上面的commit,它内部直接获取handler函数就执行了.假如我们在mutations函数里面定义异步操作来改变state会怎样呢?

从源码角度来看,定义异步操作完全没有问题,最终修改state里面的数据也会让页面重新渲染.但是由于它内部没有像dispatch里面的Promise化处理,致使调试工具无法正确记录异步操作引起的状态变化,这样不利于开发环节对程序整个运行流程的把控和错误的追踪,因此官方不建议在mutations函数里面写异步操作.

//页面
this.$store.dispatch("getList").then(()=>{
  // action异步操作完毕了

})

//vuex里面actions的定义
{
  actions:{
    getList({state,commit}){
        //这里要返回一个Promise页面那边才监听的到异步操作是否完成
        return new Promise((resolve)=>{ 
               ajax(...).then((res)=>{
                    commit(res);
                    resolve(res);
               }) 
        })
        
    }
  }
}

到目前为止,已经将dispatch,commit,actions,mutations如何协作修改数据的流程介绍完毕了,但在这个环节当中,local对象里面四个属性的实现没有展开讲,现在来看一下它的实现原理.

local实现

local对象是运行makeLocalContext(store, namespace, path)返回的结果,它最终会被存储在module对象下面的context属性里.

local对象对外暴露了四个属性分别是dispatch,commit,gettersstate.这四个属性只服务于当前模块(代码如下).

先看localdispatch的实现,它首先判断noNamespace是否存在,如果noNamespace不存在说明当前的模块是根模块,直接调用全局store对象的dispatch方法.

如果存在说明当前是一个子模块,那么此时localdispatch对应的是一个函数,该函数的参数也包含typepayload.

但是我们会发现它里面并没有实现dispatch的细节,函数内部仅仅只是将typenamespace做了字符串拼接,最终调用的还是全局的store.dispatch方法(commit处理和dispatch类似,不再赘述).

//store是使用new Vuex.Store({...})最终返回的实例对象

function makeLocalContext (store, namespace, path) { 

  var noNamespace = namespace === '';

  var local = {
    dispatch: noNamespace ? store.dispatch : function (_type, _payload, _options) {
      var args = unifyObjectStyle(_type, _payload, _options);
      var payload = args.payload;
      var options = args.options;
      var type = args.type;

      if (!options || !options.root) {
        type = namespace + type;
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : function (_type, _payload, _options) {
      var args = unifyObjectStyle(_type, _payload, _options);
      var payload = args.payload;
      var options = args.options;
      var type = args.type;

      if (!options || !options.root) {
        type = namespace + type;
      }

      store.commit(type, payload, options);
    }
  };

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? function () { return store.getters; }
        : function () { return makeLocalGetters(store, namespace); }
    },
    state: {
      get: function () { return getNestedState(store.state, path); }
    }
  });

  return local
}

local对象里的dispatchcommit仅仅只是对type做了一下拼接,最终调用的还是全局store下的dispatchcommit.

现在再看gettersstate.模块下的state是通过调用getNestedState(store.state, path)返回相应的值(代码如下).

//获取子级的state,那这个path对应着模块名称组合的数组
function getNestedState (state, path) {
  return path.length
    ? path.reduce(function (state, key) { return state[key]; }, state)
    : state
}

store.state是全局store下定义的根模块的状态(后面会介绍),通过path数组可以从最上层的根模块往下寻找,直到找到子模块返回.如果path是空数组,直接返回根模块的状态.

local下的getters只会触发该模块定义的getters函数(代码如下).它的实现原理是直接遍历全局store对象下getters对象定义的所有getter函数(store.getters的实现后面介绍),然后只取出属于当前模块的getters函数的函数名,再设置一层代理通过函数名去调用store.getters对象的函数,store.getters底层是从store._wrappedGetters获取处理函数的.

function makeLocalContext(){
    ...
    Object.defineProperties(local, {
        getters: {
          get: noNamespace
            ? function () { return store.getters; }
            : function () { return makeLocalGetters(store, namespace); }
        }
    });
    return local;
}
// 假设获取子模块home的getters函数getList,最终被转化成获取根模块的getters的"home/getList"
function makeLocalGetters (store, namespace) {
  var gettersProxy = {};

  var splitPos = namespace.length;
  
  Object.keys(store.getters).forEach(function (type) {

    if (type.slice(0, splitPos) !== namespace) { return }
    
    var localType = type.slice(splitPos);
    
    Object.defineProperty(gettersProxy, localType, {
      get: function () { return store.getters[type]; },
      enumerable: true
    });
  });

  return gettersProxy
}

到目前为止installModule安装模块的流程执行结束了,installModule执行完后store对象的数据结构变成了如下的样子,下一步执行resetStoreVM(this, state)完成数据的响应化处理.

{
   ...,
   _actions:{
        home/getList: [ƒ],  //home模块下actions中定义的getList函数
        home/getNav: [ƒ], // home模块下actions中定义的getNav函数
        getUser: [ƒ] //根模块下actions中定义的getUser函数
   },
   _mutations:{
        logout: [ƒ], // 根模块下mutations中定义的logout函数 
        home/setList: [ƒ], // home模块下mutations中定义的setList函数 
        home/setNav: [ƒ] //  home模块下mutations中定义的setNav函数 
   },
   _wrappedGetters:{
        home/getFilterList: ƒ wrappedGetter(store) // home模块下getters中定义的函数 
   },  
  _modules:{
        root:{
                context: {dispatch: ƒ, commit: ƒ, getter,state},//根模块的local对象
                state:{
                    home: {list:[]} // home模块下的 list
                    userInfo: {}  // 根模块下的 userInfo
                },
                _children:
                  {  
                    home: {
                        context: {dispatch: ƒ, commit: ƒ,getter,state}, //home模块的local对象
                        state: {list:[]},
                        _children: {},
                        _rawModule: {...} //原始配置
                    }
                 },
                 _rawModule: {...}, //原始配置
               }
       }
}

状态响应化

var Store = function Store (options) {
    
  ...
  
  this._modules = new ModuleCollection(options);
  
  // 获取根模块的状态
  var state = this._modules.root.state;
  
  //安装模块
    
  installModule(this, state, [], this._modules.root);
  
  //响应化处理
  resetStoreVM(this, state);
  
  ...

}

在讲解installModule过程可知,根模块root对应的state是一个包含了父子级的层级结构,每一级都拥有自己的state状态(代码如下).

store._module = {
  root:{
     state:{
        user_info:null,//user_info是根模块的状态信息
        home:{  //list是home模块(子模块)的状态,以子模块的名称为``key``,状态为值赋予父模块的状态对象上
          list:[]
        }
     }
  }
}

现在将根模块的状态state传入resetStoreVM函数做响应化处理.我们在上面讲local里面getters实现的时候,发现它最终调用的是store.getters,下面代码里便包含了store.getters的定义.

store._wrappedGetters里面存储着所有模块定义的getters处理函数,它首先遍历wrappedGetters对象,获取函数名key和处理函数fn.然后将keyfn重新组合成函数放入computed对象中.

resetStoreVM执行响应化最关键的代码是重新构建了Vue实例并赋值给了store._vm,同时将根模块状态state作为初始值赋予了$$state,computed作为计算属性也赋值给了_vm.

store.getters通过Object.defineProperty重新设置get方法,因此从store.getters获取getters转向了从computed中获取,继而转向了从store._wrappedGetters中拿处理函数.

function resetStoreVM (store, state, hot) {
   ...
  store.getters = {};
  var wrappedGetters = store._wrappedGetters;
  var computed = {};
  forEachValue(wrappedGetters, function (fn, key) {
    computed[key] = function () {
       return fn(store);
    }
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; },
      enumerable: true // for local getters
    });
  });
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
  ...
}

目前为止,store对象下的gettersdispatch以及commit的实现都介绍完毕,但还有一个最重要的store.state没有提及.

源码里面是通过下面的方式来定义store.state,它直接屏蔽了set修改值的方式,所以直接修改store的状态就会发出警告.

get函数则被代理指向了this._vm._data.$$state.从这里可以看出store的数据状态是存放在另外一个Vue实例的$$state中的,因此store.state便具有了响应式特性.

var prototypeAccessors$1 = { state: { configurable: true } };

prototypeAccessors$1.state.get = function () {
  return this._vm._data.$$state
};

prototypeAccessors$1.state.set = function (v) {
  if (process.env.NODE_ENV !== 'production') {
    assert(false, "use store.replaceState() to explicit replace store state.");
  }
};

Object.defineProperties( Store.prototype, prototypeAccessors$1 );

store对象分发

Vue.use(Vuex)触发applyMixin执行安装操作(代码如下).applyMixin函数里面主要添加了一个mixin让每一个新创建的vue实例都会去执行vuexInit函数.

vuexInit函数执行完毕后,所有Vue实例都在自己的作用域内创建了一个$store变量,这些$store都指向了从顶层初始化根实例时传入的store对象.


function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit });
  } 

  function vuexInit () {
    var options = this.$options;
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store;
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store;
    }
  }
}

API实现

mapState

我们先看mapState在页面上有几种常用的调用方式.

 computed:{
       ...mapState("home",{ //获取home模块下的list
           list:"list"
       })
 }
 ...
 computed:{
       ...mapState({
           list:state=>state.home.list //获取home模块下的list
       })
 }
 ...
 computed:{
       ...mapState(["login_info"]) //获取根模块下的login_info
 }

通过上面调用方式推测可知,mapState执行最终返回一个对象,对象里面key为字符串,值为一个函数,用...解构放入computed中.

normalizeNamespace主要对namespace做了一下处理,如果采用上面第一种调用方式,传递了namespacehome,那么normalizeNamespace处理后的包裹的函数里,namespace变成了home/.

下面代码里res正是mapState最终返回的对象,key对应着计算属性调用的key,value为另外的一层封装函数mappedState.

mappedState函数首先会判断有没有发送模块名称namespace,如果namespace存在,就从子模块对象中通过context属性拿到该模块的local对象,那么从local对象取出来的stategetters只属于该模块.从而返回的结果也是从子模块中取出来的值.倘若namespace不存在,就直接从根模块下的state取值.

var mapState = normalizeNamespace(function (namespace, states) {
    var res = {};
    normalizeMap(states).forEach(function (ref) {
      var key = ref.key;
      //上面案例中,val根据调用方式不同,可能为``list``字符串,也可能是...mapState({list:fn})中的函数fn
      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
});

mapGetter

mapGetter调用方式和mapState类似,也是在页面组件的computed中解构使用.

mapGetters返回一个对象res,对象的key对应着计算属性调用的key,value为另外的一层封装函数mappedGetter.

mappedGetter函数执行的结果指向全局store对象下定义的getters.

var mapGetters = normalizeNamespace(function (namespace, getters) {
    var res = {};
    normalizeMap(getters).forEach(function (ref) {
      var key = ref.key;
      var val = ref.val;

      val = namespace + val;
      res[key] = function mappedGetter () {
        if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
          return
        }
        return this.$store.getters[val]
      };

    });
    return res
});

mapAction 和 mapMutations

mapActions通常使用的调用方式如下代码,将根模块下定义的actions里的函数getUser提取到组件methods中调用.

 methods:{
    ...mapActions(['getUser'])
 }

mapActions调用时被解构在Vue组件的methods里,因此可以推测出mapActions函数返回值也是一个对象.

观察下面代码.在使用上面的调用方式过程中,refkeyval都为字符串getUser.resmapActions执行完毕后最终返回的对象,其中以ref.key作为键,mappedAction函数作为值.

mappedAction函数首先会判断有没有传递模块名称namespace,如果namespace不存在就从全局store对象下面获取dispatch,否则就从子模块下的local中获取dispatch.

dispatch执行时传入val和参数args,这就相当于模拟调用dispatch(type,payload)的操作来触发actions函数执行.

mapMutation的代码和mapAction类似,可按照相同思路分析.

var mapActions = normalizeNamespace(function (namespace, actions) {
var res = {};
normalizeMap(actions).forEach(function (ref) {
  var key = ref.key;
  var val = ref.val; // actions里面定义的函数名

      res[key] = function mappedAction () {
            var args = [], len = arguments.length;
            while ( len-- ) args[ len ] = arguments[ len ];

            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
});

尾言

本文从数据的模块化处理,响应式改造以及常用API的实现来探索了vuex实现的整体脉络.

从上面篇幅可以看出来,vuex里面大部分处理逻辑都是为了支持复杂的多模块数据结构.如果没有多模块的引入,它里面的响应式设计是非常容易实现的.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值