Vuex原理解析

vuex原理

你真的懂vuex吗?先抛出几个问题?

  1. 命名空间的原理?
  2. 辅助函数的原理?
  3. 插件用法是否了解?
  4. 为什么每个组件都能访问到$store这个实例属性?
  5. 为什么访问this.$store.getters[‘a/xx’]而不是this.$store.getters.a.xx?
  6. state数据修改后,视图中的getters数据为何也会动态改变
  7. actions里是否可以直接操作state?(strict为ture即严格模式下不行,那在严格模式下又是如何做到只能使用commit修改?)
  8. actions里如何访问别的modules中的state?

ok,答案都在下面噢👇

注册插件

这个就不用多说了,注册vue插件的常用方法通过vue.use()进行,传入的可以是对象或者函数,对象的话必须包含install方法,内部会调用install方法,并将vue作为参数传入

install方法
function install (_Vue) {
  //检查是否已经注册过,注册过的话全局变量Vue会被赋值为Vue构造函数
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      );
    }
    return
  }
  Vue = _Vue;
  applyMixin(Vue);
}

applyMixin

全局混入生命周期,使得每个组件能访问到$store

function applyMixin (Vue) {
  var version = Number(Vue.version.split('.')[0]);
  //判断版本号
  if (version >= 2) {
    //使用mixin混入beforeCreate生命周期
    Vue.mixin({ beforeCreate: vuexInit });
  } else {
    var _init = Vue.prototype._init;
    Vue.prototype._init = function (options) {
      if ( options === void 0 ) options = {};

      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit;
      _init.call(this, options);
    };
  }
  
  //使每个组件都能访问到$store
  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;
    }
  }
}

store构造函数

class Store{
    constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
    const {
      plugins = [],
      strict = false
    } = options

    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    //用来记录提交状态,主要用来检查是否正在进行commit操作  
    this._committing = false
    //存储actions
    this._actions = Object.create(null)
    //存储mutations
    this._mutations = Object.create(null)
    //存储getters
    this._wrappedGetters = Object.create(null)
    //存储modules
    this._modules = new ModuleCollection(options)
    //存储module和其namespace的对应关系。
    this._modulesNamespaceMap = Object.create(null)
    //订阅监听
    this._subscribers = []
    //监听器
    this._watcherVM = new Vue()

    const store = this
    const { dispatch, commit } = this
    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)
    }
    this.strict = strict
    installModule(this, state, [], this._modules.root)
    resetStoreVM(this, state)
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
}

1. 主要片段1

const { dispatch, commit } = this
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)
}

上面这段代码是啥意思呢?

主要解决的问题是调用commit的时候this的指向问题,比如下面这段代码

actions:{
	addSalaryAction({commit},payload){
		setTimeout(()=>{
			commit('addSalary',payload)
		},1000)
	}
}

调用commit的时候,只是执行commit这个方法,this指向的并不是store实例,当然解决方案有很多,源码做了科里化,这样执行commit的时候都会返回一个用store实例调用的结果

commit
=>
function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

经典!!!nice!!!

2. 插件管理

没错store内部也是有插件可以定制的

用法如下:

import MyPlugin=()=>store=>{
  store.subscribe(xx),
  store.watch(xx)
}
const store = new Vuex.Store({
  plugins: [MyPlugin()]
}

源码很简单:

//查看是否传入plugins
var plugins = options.plugins; if ( plugins === void 0 ) plugins = [];
//遍历plugins传入store实例作为参数
plugins.forEach(function (plugin) { return plugin(this$1); });

3.resetStoreVM

function resetStoreVM (store, state, hot) {
  var oldVm = store._vm;//在store中定义的vue实例

  // 创建getters
  store.getters = {};
  // 重置缓存
  store._makeLocalGettersCache = Object.create(null);
  var wrappedGetters = store._wrappedGetters;
  var computed = {};
  // 为getters设置代理 4.2会着重讲到
  forEachValue(wrappedGetters, function (fn, key) {
    computed[key] = partial(fn, store);
    Object.defineProperty(store.getters, key, {
      get: function () { return store._vm[key]; },
      enumerable: true // 可枚举
    });
  });

  
  var silent = Vue.config.silent;
  Vue.config.silent = true;
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed: computed
  });
  Vue.config.silent = silent;

  // 严格模式只能通过commit修改state
  if (store.strict) {
    enableStrictMode(store);
  }
  //是否有旧的实例
  if (oldVm) {
    if (hot) {
      store._withCommit(function () {
        oldVm._data.$$state = null;
      });
    }
    Vue.nextTick(function () { return oldVm.$destroy(); });
  }
}

enableStrictMode

function enableStrictMode (store) {
  //监听state的变化,保证只能使用commit修改state
  store._vm.$watch(function () { return this._data.$$state }, function () {
    if (process.env.NODE_ENV !== 'production') {
      //通过实例属性_commiting来判断,只要不经过commit,_commiting就为false,就会报错
      assert(store._committing, "do not mutate vuex store state outside mutation handlers.");
    }
  }, { deep: true, sync: true });
}

_withCommit

做实例属性_commiting状态的改变,主要用来在严格模式下确保只能通过commit修改state,因为只要通过commit修改_commiting属性就会发生改变

Store.prototype._withCommit = function _withCommit (fn) {
  var committing = this._committing;
  this._committing = true;
  fn();
  this._committing = committing;
};

4. 主要API

4.1 state的原理

如何通过访问this.$store.state.xx就能访问对应state的值?并且改变state后视图也能对应渲染

很简单,创建一个vue实例,state作为data中的数据即可

class Store {
    constructor(options) {
        this._vm = new Vue({
            data() {
                return {
                    state: options.state
                }
            }
        })
    }
    get state(){
    	return this._vm.state
    }
    //直接设置state会报错
    set state(v){
      throw new Error('')
    }
}
4.2 getters?

如何通过访问this.$store.getters.a就能返回对应方法执行后的返回值?

也就是this.$store.getters.a => return this.$store.getters.a(state)

很简单做一层代理即可

Object.defineProperty(store.getters, key, {
  get: function () { return store._vm[key]; },
  enumerable: true // for local getters
});

这里最重要一点也是面试经常会问到的修改state之后getters的视图数据如何动态渲染?

源码中使用的方法是将其放到vue实例的computed中

var wrappedGetters = store._wrappedGetters;
//拿到存储的所有getters
var computed = {};
//遍历getters
forEachValue(wrappedGetters, function (fn, key) {
    //存储到computed对象中
    computed[key] = partial(fn, store);//partical的作用是将其变成()=>{fn(store)}
    //设置getters的代理,访问getters就是访问computed
    Object.defineProperty(store.getters, key, {
        get: function () { return store._vm[key]; },
        enumerable: true
    });
});
...
store._vm = new Vue({
    data: {
      $$state: state
    },
    //赋值给计算属性
    computed: computed
});

wrappedGetters是安装的所有getters的值,需要在installModule中看,下面会提到

4.3 commit

提交mutations

commit是store的实例方法,commit传入key值和参数来执行mutations中对应的方法

几种不同的用法:

  • commit({type:xx,payload:xx})
  • commit(type, payload)

我们看下源码:

Store.prototype.commit = function commit (_type, _payload, _options) {
  var this$1 = this;
  //对传入的参数做统一处理,因为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];
  //检查是否有对应的mutations
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] unknown mutation type: " + type));
    }
    return
  }
  //改变commiting状态
  this._withCommit(function () {
    entry.forEach(function commitIterator (handler) {
      handler(payload);
    });
  });
  //发布所有订阅者
  this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); });

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      "[vuex] mutation type: " + type + ". Silent option has been removed. " +
      'Use the filter functionality in the vue-devtools'
    );
  }
};
4.4 dispatch

分发actions

执行异步

 actions: {
    minusAgeAction({commit},payload){
      console.log(this)
      setTimeout(()=>{
        commit('minusAge',payload)
      },1000)
    }},

有一个注意点是actions中的commit中的this指向问题,上面已经讲过了,我们看下核心源码:

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];
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] unknown action type: " + type));
    }
    return
  }

  try {
    this._actionSubscribers
      .filter(function (sub) { return sub.before; })
      .forEach(function (sub) { return sub.before(action, this$1.state); });
  } catch (e) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn("[vuex] error in before action subscribers: ");
      console.error(e);
    }
  }

  var result = entry.length > 1
    ? Promise.all(entry.map(function (handler) { return handler(payload); }))
    : entry[0](payload);

  return result.then(function (res) {
    try {
      this$1._actionSubscribers
        .filter(function (sub) { return sub.after; })
        .forEach(function (sub) { return sub.after(action, this$1.state); });
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn("[vuex] error in after action subscribers: ");
        console.error(e);
      }
    }
    return res
  })
};

modules

最基本的用法:

import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    age: 10,
    salary: 6500
  },
  getters: {
    totalSalary(state) {
      return state.salary * 12
    }
  },
  modules: {
    a: {
      namespaced: true,
      state: {
        age: 100,
        salary:12000
      },
      getters:{
        totalSalary(state){
          return state.salary*12
        }
      },
      modules:{
        b: {
          namespaced:true,
          state:{
            age:200
          }
        }
      }
    }
  }
})

访问a中的state=>this.$store.state.a.age

访问a中的getters=>this.$store.getters[‘a/totalSalary’]

访问a中子模块b中的state=>this.$store.state.a.b.age

访问a中子模块b中的getters=>this.$store.getters[‘a/b/totalSalary’]

vuex中是如何管理module的?

1.ModuleCollection

这个类的作用就是将传入的modules格式化便于管理,比如传入以下的modules

modules:{
    a: {
      namespaced: true,
      state: {
        age: 100,
        salary: 12000
      },
      getters: {
        totalSalary(state) {
          return state.salary * 12
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            age: 300,
            salary: 14000
          },
          getters: {
            totalSalary(state) {
              return state.salary * 12
            }
          },
        }
      }
    },
    b:{}
 }

=》格式化成:

root:{
  _raw:rootModule,
  _children:{
		a:{
      _raw:aModule,
      _children:{
        c:{
          _raw:bModule,
          state:cState
        }
      },
      state:aState
    },
    b:{
      _raw:bModule,
      state:bState
    }
  },
  state:xx
}

实现起来很简单,但是这里有一个bug就是无法递归到第二层级,就是说下面的代码转化的a和c在同一层级,但很明显的是c是a的children?如何改进呢?小伙伴们可以先试着想一下

class ModuleCollection{
    constructor(options){
        this.register([],options)
    }
    register(path,rootModule){
        let newModule={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        }
        if(path.length===0){
            this.root=newModule
        }else{
            this.root._children[path[path.length-1]]=rootModule
        }
        
        if(rootModule.modules){
            forEach(rootModule.modules,(key,value)=>{
                this.register(path.concat(key),value)
            })
        }
    }
}

改进过后:

class ModuleCollection{
    constructor(options){
        this.register([],options)
    }
    register(path,rootModule){
        let newModule={
            _rawModule:rootModule,
            _children:{},
            state:rootModule.state
        } //vuex源码是将其变成一个module类
        
        if(path.length===0){
            this.root=newModule
        }else{
            //第一次path为[a]
            //第二次path变成[a,c]=>所以c前面的必然是父级,但也可能存在多个层级及父亲的父亲(及如果是[a,c,d]的话那么意味着a是d的父亲的父亲),所以需要用reduce
            //然后又回到与a平级,于是path重新变成[b]
            let parent=path.slice(0,-1).reduce((prev,curr)=>{
                return prev._children[curr]
            },this.root) //vuex源码中这一段会封装成一个get实例方法
            
            parent._children[path[path.length-1]]=newModule //vuex源码这一段会作为module类的一个实例属性addChild,因为parent是一个module类所以可以调用addChild方法
        }
        if(rootModule.modules){
            forEach(rootModule.modules,(key,value)=>{
                this.register(path.concat(key),value)
            })
        }
    }
}

2.installModule

modules定义完毕,下一步就是如何在$store上取值了,比如想要调用a中的state或者getters?如何做呢,很简单我们需要将整个module安装到$Store上

2.1 安装state

在实现之前我们先看下用法,比如有我们注册了这样的module

export default new Vuex.Store({
  state: {
    age: 10,
    salary: 6500
  },
  getters: {
    totalSalary(state) {
      return state.salary * 12
    }
  },
  mutations: {
    addAge(state, payload) {
      state.age += payload
    },
    minusAge(state, payload) {
      state.age -= payload
    }
  },
  actions: {
    minusAgeAction({ commit }, payload) {
      setTimeout(() => {
        commit('minusAge', payload)
      }, 1000)
    }
  },
  modules: {
    a: {
      namespaced:true,
      state: {
        age: 100,
        salary: 10000
      },
      getters: {
        totalSalaryA(state) {
          return state.salary * 12
        }
      },
      mutations: {
        addAge(state, payload) {
          console.log(1)
          state.age += payload
        },
        minusAge(state, payload) {
          state.age -= payload
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            age: 300,
            salary: 14000
          },
          getters: {
            totalSalaryC(state) {
              return state.salary * 12
            }
          },
          modules:{
            d:{}
          }
        }
      }
    },
    b:{}
  }
})

我们希望访问this.$store.state.a.age就能拿到a模块下的age,原理其实和之前格式化的原理一样,格式化是需要递归然后把子模块放到父模块的_children中,而安装state则是将子模块中的state都放到父级的state中

const installModule=(store,state,path,rootModule)=>{
	if(path.length>0){
    let parent=path.slice(0,-1).reduce((prev,curr)=>{
      return prev[curr].state
    },state)
    parent[path[path.length-1]]=rootModule.state
  }
  forEach(rootModule._rawModule.modules,(key,value)=>{
    installModule(store,state,path.concat(key),value)
  })
}

如果不是很理解可以先打印path,看每一次path的取值就会明朗很多,这样就结束了吗?并没有那么简单,细心的小伙伴会发现我们store类中的state是响应式的,是基于vue的发布订阅模式,就是一旦state中值发生改变会触发试图的更新但是我们上述是在state中又增加了n个对象,这些对象并不是响应式的(如果了解vue的原理这一块不用多讲),意思就是后续增加的对象改变不会触发试图的更新,所以我们需要将这些后增加的也变成响应式的,很简单,vue中有静态方法set可以帮您轻松搞定

parent[path[path.length-1]]=rootModule.state
=>
Vue.set(parent,path[path.length-1],rootModule.state)
2.2 安装getters

getters的用法比较特殊

1.是不能注册相同名称的方法

2.如果没有注册命名空间,想获取a模块中的getters中的方法,用法为this.$store.getters.属性名

3.如果注册了命名空间,想获取a模块中的getters中的方法,用法为this.$store.getters['a/xx']

我们先看前两个如何实现?

const installModule=(store,state,path,rootModule)=>{
    let getters=rootModule._rawModule.getters
    //当前的module是否存在getters
    if(getters){
        //如果存在把getters都挂载到store.getters上也就是Store类的实例上
        forEach(getters,(key,value)=>{
            Object.defineProperty(store.getters,key,{
                get:()=>{
                    return value(rootModule.state)
                }
            })
        })
    }
    //递归module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
  	})
}

####2.3 安装mutations

mutations的用法和getters又不同

1.可以注册相同名称的方法,相同方法会存入到数组中按顺序执行

2.如果没有注册命名空间,想获取a模块中的mutations中的方法,用法为this.$store.commit('属性名')

3.如果注册了命名空间,想获取a模块中的mutations中的方法,用法为this.$store.commit['a/xx']

我们先看前两个如何实现?

const installModule=(store,state,path,rootModule)=>{
    let mutations=rootModule._rawModule.mutations
    //当前的module是否存在mutations
    if(mutations){
        //如果存在,看当前的属性在mutations中是否已经存在,不存在的话先赋值为空数组,然后在将方法放入,出现同名也没关系,调用时依次执行
        forEach(mutations,(key,value)=>{
    			if (mutations) {
        		forEach(mutations, (key, value) => {
            		store.mutations[key]=store.mutations[key]||[]
            		store.mutations[key].push(
                	payload => {
                    value(rootModule.state, payload)
                	}
            		)
        		})
    			}
        })
    }
    //递归module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
  	})
}
2.4 安装actions

actions和mutations原理几乎一模一样,主要区别如下

1.调用时接受的参数不是state而是一个经过处理的对象可以解构出state,commit等属性

2.调用时没有命名空间是this.$store.dispatch(xx),有命名空间是this.$store.dispatch('a/xx')

我们先看前两个如何实现?

const installModule=(store,state,path,rootModule)=>{
    let actions=rootModule._rawModule.actions
    //当前的module是否存在actions
    if(actions){
        //如果存在,看当前的属性在actions中是否已经存在,不存在的话先赋值为空数组,然后在将方法放入,出现同名也没关系,调用时依次执行
        forEach(actions,(key,value)=>{
            store.actions[key]=store.actions[key]||[]
            store.actions[key].push(
                payload => {
                    value(store, payload)
                }
            )
        })
    }
    //递归module
    forEach(rootModule._rawModule.modules,(key,value)=>{
    	installModule(store,state,path.concat(key),value)
  	})
}

###3.命名空间

也就是所谓的namespace,为什么需要命名空间?

1.不同模块中有相同命名的mutations、actions时,不同模块对同一 mutation 或 action 作出响应

2.辅助函数(下面会讲到)

在上面安装模块的时候我们讲过mutations和actions中如果名称相同并不矛盾,会将其都存入到数组中依次执行,那这样带来的后果就是我只想执行a模块中的mutation但是没想到先执行了最外面定义的mutation,然后在执行a模块中的,所以我们需要引入命名空间加以区分(有人说那我起名字不一样不就行了吗?相信我项目大或者多人合作的话这一点真的很难保证)

我们先看下命名空间的API:

比如我们想要调用a模块中mutations的addNum方法,我们一般这样调用this.$store.commit('a/addNum'),我们之前调用是this.$store.commit('addNum')

所以我们只需要在安装mutations时把对应属性前加一个模块名的前缀即可。整理步骤如下

  • 先判断该模块是否有命名空间,如果有的话需要进行拼接返回,比如a模块有命名空间返回a/,a模块下的c模块也有则返回a/c/
  • 将返回的命名空间放在mutations或者actions属性名的前面加以区分

我们看下vuex内部源码

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
  var module = this.root;
  return path.reduce(function (namespace, key) {
    module = module.getChild(key);
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
};
//getNamespace是实例方法用来返回拼接后的命名空间
...
function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length;
  var namespace = store._modules.getNamespace(path);

  if (module.namespaced) {
    //如果有命名空间,则存放到_modulesNamespaceMap对象中
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(("[vuex] duplicate namespace " + namespace + " for the namespaced module " + (path.join('/'))));
    }
    store._modulesNamespaceMap[namespace] = module;
  }

  if (!isRoot && !hot) {
    var parentState = getNestedState(rootState, path.slice(0, -1));
    var moduleName = path[path.length - 1];
    store._withCommit(function () {
      if (process.env.NODE_ENV !== 'production') {
        if (moduleName in parentState) {
          console.warn(
            ("[vuex] state field \"" + moduleName + "\" was overridden by a module with the same name at \"" + (path.join('.')) + "\"")
          );
        }
      }
      Vue.set(parentState, moduleName, module.state);
    });
  }

  var local = module.context = makeLocalContext(store, namespace, path);

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

  module.forEachChild(function (child, key) {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}

ok这样基本就实现了,但是actions似乎还有问题,我们举个例子

actions: {
    minusAgeAction({ commit }, payload) {
      setTimeout(() => {
        commit('minusAge', payload) //并没有使用命名空间,如何精确调用b模块mutation?
      }, 1000)
    }
},

假设上面是a模块的actions,我们调用时执行的commit并没有使用命名空间调用,所以势必又会去调用最外层的mutation?所以根源是解构出的commit有问题,我们看下vuex源码这块是怎么写的?

function installModule (store, rootState, path, module, hot) {
  var isRoot = !path.length;
  var namespace = store._modules.getNamespace(path);
  ...

  var local = module.context = makeLocalContext(store, namespace, path);
  //创建上下文对象代替store实例
  ...

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


  module.forEachChild(function (child, key) {
    installModule(store, rootState, path.concat(key), child, hot);
  });
}

registerAction

function registerAction (store, type, handler, local) {
  var entry = store._actions[type] || (store._actions[type] = []);
  entry.push(function wrappedActionHandler (payload) {
    //传入上下文对象
    var res = handler.call(store, {
      dispatch: local.dispatch,//只能调用当前模块的actions
      commit: local.commit,//只能调用当前模块的mutations
      getters: local.getters,//只包含当前模块的getters
      state: local.state,//只包含当前的state
      rootGetters: store.getters,//根上的getters,可用来调取别的模块中的getters
      rootState: store.state//根上的state,可用来调取别的模块中的state
    }, payload);
    if (!isPromise(res)) {
      res = Promise.resolve(res);
    }
    if (store._devtoolHook) {
      return res.catch(function (err) {
        store._devtoolHook.emit('vuex:error', err);
        throw err
      })
    } else {
      return res
    }
  });
}

创建上下文对象的方法

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;
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(("[vuex] unknown local action type: " + (args.type) + ", global type: " + type));
          return
        }
      }

      return store.dispatch(type, payload)
    },

    //调用a模块中的addAge,commit('addAge',payload)就相当于调用store.commit('a/addAge',payload)
    commit: noNamespace ? store.commit : function (_type, _payload, _options) {
      //判断是否传入的对象
      //另外一种调用方式, 传入的是一个对象 commit({type:xx,payload:xx})
      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;
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(("[vuex] unknown local mutation type: " + (args.type) + ", global type: " + type));
          return
        }
      }
      //调用实例中的commit
      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
}

unifyObjectStyle

function unifyObjectStyle (type, payload, options) {
  //判断传入的第一个参数是否是对象,对参数重新做调整后返回
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
  return { type, payload, options }
}

4.subscribe

这个API可能很多人不常用,主要使用场景就是监听commit操作,比如我需要将哪些state值动态本地存储,即只要调用commit就执行本地存储将新的值更新。当然还有很多使用场景

Store.prototype.commit = function commit (_type, _payload, _options) {
  ...
  this._subscribers.forEach(function (sub) { return sub(mutation, this$1.state); });
  ...
};

从上面代码可以看出只要执行commit操作便会执行实例属性_subscribers,没错这就是发布订阅的模式。先收集所有订阅内容,执行commit就遍历执行

store实例方法subscribe (发布)

Store.prototype.subscribe = function subscribe (fn) {
  return genericSubscribe(fn, this._subscribers)
};

genericSubscribe(订阅)

function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn);
  }
  return function () {
    var i = subs.indexOf(fn);
    if (i > -1) {
      subs.splice(i, 1);
    }
  }
}

demo

this.$store.subscribe(()=>{
	console.log('subscribe')
})

每执行一次commit便会打印subscribe

当然还有action的监听,可以自己研究下

5.辅助函数

方便调用的语法糖

5.1 mapState

原理其实很简单

比如在a模块调用age,想办法将其指向this.$store.state.a.age

用法

import {vuex} from "vuex"
export default:{
	computed:{
		...mapState('a',['age'])
    //...mapState('a',{age:(state,getters)=>state.age) 另外一种调用方法
	}
}

从上面调用方法可知,mapState是一个函数,返回的是一个对象,对象的结构如下:

{
	age(){
		return this.$store.state.a.age
	}
}
=》
computed:{
	age(){
		return this.$store.state.a.age
	}
}

于是我们调用this.age就可获得当前模块的state中的age值,好的我们看下源码

var mapState = normalizeNamespace(function (namespace, states) {
  var res = {};
  //传入的states必须是数组或对象
  if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object');
  }
  //遍历先规范化states,然后做遍历
  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) {
        // 获取实例上_modulesNamespaceMap属性为namespace的module
        var module = getModuleByNamespace(this.$store, 'mapState', namespace);
        if (!module) {
          return
        }
        //module中的context是由makeLocalContext创建的上下文对象,只包含当前模块
        state = module.context.state;
        getters = module.context.getters;
      }
      //判断val是否是函数,这里涉及到mapState的不同用法,核心的语法糖
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    };
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res
});
5.2 mapGetters

原理和mapState几乎一模一样

比如在a模块调用totalSalary,想办法将其指向this.$store.getters[‘a/totalSalary’]

var mapGetters = normalizeNamespace(function (namespace, getters) {
  var res = {};
  if (process.env.NODE_ENV !== 'production' && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object');
  }
  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
      }
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(("[vuex] unknown getter: " + val));
        return
      }
      return this.$store.getters[val] //这个在上面命名空间中的getters已经讲过了,核心语法糖
    };
    //调试用的
    res[key].vuex = true;
  });
  return res
});
5.3 mapMutations

用法

...mapMutations('a',['addAge'])
//...mapMutations('a',{
//    addAge:(commit,payload)=>{
//      commit('addAge',payload)
//    }
//第二种调用方法
})
var mapMutations = normalizeNamespace(function (namespace, mutations) {
  var res = {};
  if (process.env.NODE_ENV !== 'production' && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object');
  }
  normalizeMap(mutations).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedMutation () {
      //拼接传来的参数
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];

      // Get the commit method from store
      var commit = this.$store.commit;
      if (namespace) {
        var module = getModuleByNamespace(this.$store, 'mapMutations', namespace);
        if (!module) {
          return
        }
        commit = module.context.commit;
      }
      //判断是否是函数采取不同的调用方式
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    };
  });
  return res
});
5.4 mapActions

同mapMutations,只要把函数内部的commit换成dispatch就行了

####5.5 辅助函数中涉及的几个方法

normalizeMap

规范化传入的参数

function normalizeMap (map) {
  if (!isValidMap(map)) {
    return []
  } //不是数组或者对象直接返回空数组
  return Array.isArray(map)
    ? map.map(function (key) { return ({ key: key, val: key }); })
    : Object.keys(map).map(function (key) { return ({ key: key, val: map[key] }); })
  //对传入的参数做处理,做统一输出
}

normalizeNamespace

标准化命名空间,是一个科里化函数

function normalizeNamespace (fn) {
  return function (namespace, map) {
    // 规范传入的参数
    // 我们一般这样调用 有模块:...mapState('a',['age'])或者 没模块:...mapState(['age'])
    if (typeof namespace !== 'string') { //没传入命名空间的话,传入的第一个值赋值给map,命名空间为空
      map = namespace;
      namespace = '';
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/';
      //传入命名空间的话,看命名空间最后是否有/,没有添加/
    }
    return fn(namespace, map)
  }
}

对传入的命名空间添加/是因为每个声明命名空间的模块,内部的getters等的属性名都经过了处理,以命名空间+key的形式存储。

参考博客-掘金

参考博客-知乎

最后

最后附上一些心得,其实源码并不难读懂,核心代码和逻辑就那么点,我们完全可以实现一个极其简易版的vuex

源码之所以看上去很繁杂是因为

  • 作者将很多方法抽离出来,方法单独命名,所以经常会发现函数里又套了n个函数,其实只是作者将重复的一些方法单独抽离出来公用而已
  • 多个API以及API的不同使用方法,比如上面提到的mapState这些辅助函数,可以有多种使用方式,还有比如commit你可以传{xx,payload}也可以传{type:xx,payload:xx},作者要做许多类似这样的处理,兼容多个用户的习惯
  • 庞大的错误处理,什么throw new Error这些东西其实也占了很大比重,好的源码设计错误处理是必不可少的,这是为了防止使用者做一些违背作者设计初衷的行为,如果错误提示设计的好是能起到锦上添花的作用的,一旦出了错误用户能一目了然哪出了bug。

据说点赞的人都会拿到高薪噢!!!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值