vuex源码浅析

概念

Vuex应用的核心就是store(仓库)。'store'基本上就是一个容器,它包含着你的应用中大部分的状态(state)。有些同学可能会问,那我定义一个全局对象,再去上层封装一些数据存取的接口不也可以吗?

Vuex和单纯的全局对象有以下两个不同:

  • Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新
  • 你不能直接改变store中的状态。改变store中的状态唯一的途径就是显式地提交mutation。这样使得我们可以方便地跟踪每一个状态的变化。

Vuex初始化

Vuex安装过程

vuex具体使用:定义配置:moduleA、moduleB

js 复制代码
import Vue from 'vue' 
import Vuex from 'vuex' 
Vue.use(Vuex) 
const moduleA = { 
  namespaced: true, 
  state: { 
    count:1 
  }, 
  mutations: { 
    increment(state) { 
      state.count ++ 
    } 
  }, 
  actions: { 
    increment(context) { 
      context.commit('increment') 
    } 
  }, 
  getters: { 
    computedCount(state) { 
      return state.count + 1 
    } 
  } 
 } 
 const moduleB = { 
   namespaced: true, 
   state: { 
     count: 1 
   }, 
   mutations: { 
     increment(state) { 
       state.count ++ 
     } 
   }, 
   actions: { 
     increment(context) { 
       context.commit('increment') 
     } 
   }, 
   getters: { 
     computedCount(state) { 
       return state.count + 1 
      } 
     } 
   } 
   const store = new Vuex.Store({ 
     modules: { 
      a: moduleA, 
      b: moduleB 
     }, 
     state: { count: 1 }, 
     actions: { increment(context) { 
       context.commit('increment') } 
      }, 
    }) 
    const app = new Vue({ el: '#app', store })

Vuex是这样一个对象

js 复制代码
export { 
  Vuex as default, 
  version, 
  Store, 
  storeKey, 
  createStore, 
  install, 
  useStore, 
  mapState, 
  mapMutations, 
  mapGetters, 
  mapActions, 
  createNamespacedHelpers, 
  createLogger 
 }

使用Vue.use注册Vuex,即执行Vuex对象上的install方法。install方法执行applyMixin方法。通过Vue.mixin在每个组件混入beforeCreate生命周期,执行vuexInit函数。如果options中传入了store,把store赋值给this.$store,如果没有store,就会找parent的store。

通过这种方式就会让每个vue实例都会拥有 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t o r e , 这样在任意组件中都可以通过 t h i s . store,这样在任意组件中都可以通过this. </math>store,这样在任意组件中都可以通过this.store访问到这个实例。在new Vue的时候会传入这个store。

js 复制代码
let Vue 
function install(_Vue) { 
  Vue = _Vue applyMixin(Vue) 
} 
function applyMixin(Vue) { 
  // 获取vue的版本 
  const version = Number(Vue.version.split('.')[0]) 
  // 通过Vue.mixin在每个组件混入beforeCreate生命周期。执行vuexInit函数 
  if(version >= 2) { Vue.mixin({beforeCreate: vuexInit}) } } 
  function vuexInit() { 
    const options = this.$options 
    if(options.store) { 
    // options中传入了store,把store赋值给this.$store, 如果没有store,就会找parent的store 
    // 通过这种方式就会让每个vue实例都会拥有$store,这样在任意组件中都可以通过this.$store访问到这个实例 
    // 在new Vue的时候会传入这个store 
    this.$store = typeof options.store === 'function' ? options.store() : options.store } else if(
    options.parent && options.parent.$store) { 
    this.$store = options.parent.$store 
   } 
  }

Store实例化过程

如果不是通过npm的方式安装vuex并引入,而是通过外链的形式引入的。在实例化Store的时候就需要手动去执行install方法,注册vuex。然后在Store上定义一些私有属性_actions、_mutations、_wrappedGetters这些都是跟我们传入的配置相关的。在之后的安装过程对这些值进行赋值。_actionsSubscribers和_subscribers分别是订阅actions和mutations变化的订阅者。this._modules是初始化modules的逻辑,之后会讲。this._watcherVM作用就是去监听一些变化。

js 复制代码
class Store { 
  constructor(options = {}) { 
   // 通过外链的形式引入store就满足以下条件,需要手动去执行install方法,注册vuex   if(!Vue && typeof window ! == 'undefined' && window.vue) { 
    install(window.vue)
 } 
 const { plugins = [], strict = false } = options 
 this._committing = false // 标识是否是通过提交一个mutation修改state的值。 this._actions = Object.create(null) 
 this._actionsSubscribers = [] // action变化的订阅者 
 this._mutations = Object.create(null) 
 this._wrappedGetters = Object.create(null) 
 this._modules = new ModuleCollection(options) 
 this._modulesNamespaceMap = Object.create(null) 
 this._subscribers = [] // mutation变化的订阅者 
 this._watcherVM = new Vue() 
 const store = this // 定义一个局部变量store缓存this 
 const {dispatch,commit} = this //通过this拿到dispatch和commit方法 // dispatch和commit就是我们通过数据改变的方法,派发一个action或者提交一个mutation,之后会详细介绍这两个方法 // 然后重新给dispatch和commit赋值,保证执行的时候它们的上下文是store实例 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 // 严格模式
 const state = this._modules.root.state // 根模块state installModule(this,state,[],this._modules.root) //作用是对 _actions、_mutations、_wrappedGetters这些属性赋值 
 resetStoreVM(this,state) // 让getters和state建立依赖关系变成响应式等逻辑 plugins.forEach(plugin => plugin(this)) // 执行插件的逻辑 
 get state() { return this._vm.data.$$state } 
 set state(v) { } 
 } 
}

modules初始化过程

在Store实例化过程中,通过new ModuleCollection(options)初始化this._modules。当我们把所有的数据都放到state下面,那么state会变得非常臃肿。所以vuex就提供了一个modules的概念,可以理解为把数据仓库store拆分为一个个子仓库。那每个子仓库就是一个module。每个module里面有自己的state、mutations、actions、getters等。这样就可以把应用数据根据业务逻辑拆分到子模块下方便做数据管理和操作的能力。下面我们看一下这个modules是如何初始化的。

js 复制代码
class ModuleCollection { 
  constructor(rawRootModule) { 
    this.register([],rawRootModule,false) 
  } 
  register(path,rawModule, runtime = true) { 
    const newModule = new Module(rawModule,runtime) // 接下来判断path,如果path的长度是0的话newModule就会作为根模块 
    if(path.length === 0) { 
     this.root = newModule 
    }else {
    // 首先根据path的前一个拿到parent 
    const parent =   this.get(path.slice(0,-1)) 
    // 调用addChild给根模块添加children,这样就建立了父子关系,也就是说通过递归调用register, // 建立一个module是的树状结构 parent.addChild(path[path.length - 1],newModule) } // 然后判断rawModule是否有modules,就会遍历modules拿到对应的module后会递归执行这个register函数 // 这个时候path就会变化,会concat module的key,就会执行到path判断的else逻辑。 
    if(rawMoudle.modules) { 
      forEachValue(rawModule.modules,(rawChildModule,key) => {   this.register(path.concat(key),rawChildModule,runtime) 
      }) 
     } 
    } 
    get(path) { 
      return path.reduce((module,key) => { 
        return module.getChild(key) 
      },this.root) 
    } 
    getNamespace(path) { 
    let module = this.root 
    return path.reduce((namespace,key) => { module = module.getChild(key) return namespace + (module.namespaced ? key + '/' : '') 
    }) 
   } 
  }

当我们执行new ModuleCollection时候会执行constructor,然后会执行register方法。通过实例化Module来管理每一个模块。每个模块的结构如下:

保留state和原始modules,维护一个_children。接下来判断path,如果path的长度是0的话newModule就会作为根模块。然后判断rawModule是否有modules,我们传入了modules: {

a: moduleA,

b: moduleB

},就会遍历modules拿到对应的module后会递归执行这个register函数。这个时候path就会变化,会concat module的key(a或者b),就会执行到path判断的else逻辑。首先根据path的前一个值拿到parent。调用addChild给模块添加children,这样就建立了模块之间的父子关系。

js 复制代码
class Module { 
  constructor(rawModule,runtime) { 
    this.runtime = runtime 
    this._children = Object.create(null) // 保存它的子module 
    this._rawModule = rawModule // 每个module的定义 
    const rawState = rawModule.state // 每个module的state保存到Module的state中   this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } addChild(key,module) {
    this._children[key] = module 
 } 
 getChild (key) { return this._children[key] } 
 forEachChild (fn) { 
   forEachValue(this._children, fn) 
 }
 forEachGetter (fn) { 
   if (this._rawModule.getters) { 
     forEachValue(this._rawModule.getters, fn) 
   } 
 } 
 forEachMutation(fn) { 
   if(this._rawModule.mutations) { 
     forEachValue(this._rawModule.mutations,fn) 
    } 
  } 
  forEachAction(fn) { 
    if(this._rawModule.actions) { 
      forEachValue(this._rawModule.actions,fn) 
     } 
  } 
}

也就是说通过递归调用register,建立一个module的树状结构。

接下来看拿到这个this._modules后如何执行installModule。

安装模块

接下来我们根据初始化的结果,递归的去安装所有的模块。实际上就是对之前定义的_actions、_mutations包括_wrappedGetters这些初始值做一些注册和赋值的操作。

js 复制代码
function installModule(store,rootState,path,module,hot) { 
  const isRoot = !path.length 
  const namespace = store._modules.getNamespace(path) 
  if(module.namesapced) { 
    store._modulesNamespaceMap[namespace] = module 
   } 
   // 遍历children进行注册的时候会走到这个逻辑 
   if(!isRoot && !hot) { 
   const parentState = getNestedState(rootState,path.slice(0,-1)) 
   const moduleNmae = path[path.length - 1] 
   store._withCommit(() => { Vue.set(parentState,modeuleName,module.state) }) } 
   const local = module.context = makeLocalContext(store,namespace,path) 
  }

根据path的长度判断是否是根模块。我们传入的moduleA、moduleB的namesapced都为true,所以namespace的值就是moduleA、moduleB的key加上'/',即a/、b/。因此_modulesNamespaceMap的值如下:

下面有一个比较关键的函数makeLocalContext,我们思考一个事情:对于我们store而言,它存储的这些,不管是getters、actions还是mutations实际上都是一个对象。对象是由key和value决定的。所以这些key是不能重复的。当我们在moduleA和moduleB中定义了同样的increment action。最终moduleA保留的是a/increment,moduleB保留的是b/increment,根模块保留的是increment,实际上就是通过namespace拼接action的名字。如下:

但是在increment函数中,同样都是通过context.commit('increment')去提交一个increment,并没有去提交一个a/increment。因为这样写是很不优雅的。那怎么样实现在某个模块中去提交mutation就是这个模块对应的mutation呢?就是通过makeLocalContext方法来实现的。

js 复制代码
function makeLocalContext(store,namespace,path) { 
  const noNamespace = namespace === '' 
  const local = { 
    dispatch: noNamespace ? store.dispatch:(_type,_payload,_options) => {  const args = unifyObjectStyle(_type,_payload,_options) 
    const {payload,options} = args 
    let {type} = args 
    if(!options || !options.root) { 
      type =namespace + type 
    } 
    return store.dispatch(type,payload) }, 
   commit: noNamespace ? store.commit : (_type,_payload,_options) => { 
   const args = unifyObjectStyle(_type,_payload,_options) 
   const {payload,options} = args 
   let {type} = args 
   if(!options || !options.root) { 
     type =namespace + type 
   } 
   return store.commit(type,payload,options) 
   } 
  }
  Object.defineProperties(local, { 
    getters: { 
      get: noNamespace ?() => store.getters :() => makeLocalGetters(store,namespace) }, 
      state: { 
        get: () => getNestedStated(store.state,path) } 
    }) 
    return local 
   } 
   function makeLocalGetters(store,namespace) { 
     const gettersProxy = {} 
     const splitPos = namespace.length 
     Object.keys(store.getters).forEach(type => { 
     if(type.slice(0,splitPos) ! == namespace) return 
     const localType = type.slice(splitPos) Object.defineProperty(gettersProxy,localType,{ 
     get:() =>store.getters[type], 
     enumerable: true }) 
    }) 
    return gettersProxy 
  }

makeLocalContext最终返回的就是local对象。重新去定义了dispatch、commit这些方法。如果namespace为空的话就是开始定义的dispatch和commit方法。如果有namespace,那么最终的type是拼接上了这个namespace。拼接完以后才会去调用store上的dispatch和commit方法。getters也是一样的,没有namespace直接调用store.getters。如果有namespace调用makeLocalGetters方法,这个方法就会遍历store上面的getters,结构如下:

对于state而言也是类似的,通过path去寻找state,state的结构如下:

对于moduleA的state就可以通过root.state.a拿到。getNestedState就是通过path一层层找到对应的state

js 复制代码
function getNestedState(state,path) { 
  return path.length ? 
    path.reduce((state,key) => state[key],state) 
    :state 
  }

Mutation/Action/Getter注册过程

Mutation注册过程

js 复制代码
module.forEachMutation((mutation,key) => { 
  const namespacedType = namespace + key 
  registerMutation(store,namespacedType,mutation,local) 
})

遍历this._rawModule.mutations,拼接namespacedType。即传入registerMutation方法的type分别是a/increment、b/increment、increment,然后调用registerMutation去注册mutation。将wrappedMutationHanler函数push到store._mutations[type]中。

js 复制代码
function registerMutation(store,type,handler,local) { 
  const entry = store._mutations[type] || (store._mutations[type] =[])   entry.push(function wrappedMutationHanler(payload) { handler.call(store,local.state,payload) 
  }) 
}

当执行wrappedMutationHanler函数的时候就会执行模块中mutation对应的方法。把local.state传入,就可以拿到模块对应的state。payload是额外的参数。最终store._mutations的结构如下:

Action 注册过程

js 复制代码
module.forEachAction((action,key) => { 
  const type = action.root ? key : namespace + key 
   const handler = action.handler || action 
   registerAction(store,type,handler,local) 
 })

遍历定义的actions,如果是根模块就不去拼接namespace属性。action也可以传入一个handler。然后执行registerAction,跟registerMutation类似。

js 复制代码
function registerAction(store,type,handler,local) { 
  const entry = store._actions[type] || (store._actions[type] =[]) 
  entry.push(function wrappedActionHandler(payload,cb){ 
    let 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 
     }) 
   }

当执行wrappedActionHandler的时候,action对应的handler执行,且传入了模块对应的dispatch、commit、getters、state以及根模块的getters、state。这就是为什么可以在action中调用当前模块的commit方法。然后把返回的值变成promise。_actions结构如下:

Getter注册过程

js 复制代码
module.forEachGetter((getter,key) => { 
  const namespacedType = namespce + key 
  registerGetter(store,dispatch,getter,local) 
 })

也是拼接namespacedType,然后执行registerGetter

js 复制代码
funciton registerGetter(store,type,rawGetter,local) {
  if(store._wrappedGetters[type]) { return } 
  store._wrappedGetters[type] = function wrappedGetter(store) { 
  return rawGetter( local.state, local.getters, store.state, store.getters ) 
  } }

当wrappedGetter函数执行的时候就返回我们定义的getters对应的方法执行结果。最终store._wrappedGetters的结构如下:

递归注册

js 复制代码
module.forEachChild((child,key) => { 
  installModule(store,rootState,path.concat(key),child,hot) 
})

遍历this._rawModule._children,这时候会把path跟key进行拼接。然后执行installModule去注册children的actions、mutations、getters等。执行installModule时候isRoot是false。就会执行下面逻辑。

js 复制代码
const parentState = getNestedState(rootState,path.slice(0,-1)) 
const moduleNmae = path[path.length - 1] 
store._withCommit(() => { Vue.set(parentState,modeuleName,module.state) })

通过getNestedState方法找到父state,通过path最后一个数据拿到moduleName。最后执行Vue.set(parentState,modeuleName,module.state) 建立父子state映射关系。

所以通过整个installModule这样一个递归执行过程,就可把module树下面的所有state、mutations、actions、getters都做一个注册。这样就建立了整个数仓。建立好数仓后就可以通过一下api去修改数仓的数据。

建立响应式

完成上面注册逻辑后,会执行resetStoreVM(this,state)。给store定义一个public getters。通常我们访问对象属性的时候不会去访问带下划线'_'的属性,vuex提供了state和getters这两个public api来访问。getters就是在这里做的定义。接下来就会根据store._wrappedGetters通过计算拿到store.getters。遍历_wrappedGetters,做了两件事情:第一、定义computed的key对应的函数,这个函数就是返回rawGetter执行的结果。第二、通过Object.defineProperty定义store.getters对象上key属性对应的get属性。

然后利用new Vue传入data和computed做响应式。并将实例赋值给store._vm。当我们访问store.state的时候实际就是访问this._vm._data.$$state。这就是为什么我们通过store.state就可以访问到根模块的state。当我们访问store.getters[key]就会拿到store._vm[key],如果计算属性中有相同key的话就会返回computed[key]执行的结果。即我们定义的getter要返回的值。

js 复制代码
function resetStoreVM(store,state,hot) { 
  const oldVm = store._vm store.getters = {} 
  const wrappedGetters = store._wrappedGetters 
  const computed = {} 
  forEachValue(wrappedGetters,(fn,key) => { 
    computed[key] = () => fn(store) 
    Object.defineProperty(store.getters,key,{ 
      get: () => store._vm[key], 
      enumerable: true 
     }) 
    }) 
    store._vm = new Vue({ 
      data: { $$state: state }, 
      computed 
     })
     if(store.strict){ 
       enableStrictMode(store) } 
       if(oldVm) { 
         if(hot) { 
           store._withCommit(() => { 
             oldVm._data.$$state = null 
            }) 
          } 
        Vue.nextTick(() => oldVm.$destory()) 
       } }

如果我们传入strict就会执行enableStrictMode

js 复制代码
function enableStrictMode(store) { 
  store._vm.$watch(function(){return this._data.$$state},() => { 
  if(process.env.NODE_ENV !== 'production') { 
   assert(store._committing,'do not mutate vuex store state outside mutation 
   handlers') } },{deep: true,sync: true}) 
 }

通过store._vm.watch去监听this._data.$state的变化。会检测store._committing是否为true,如果不为true就会报警告:不能通过mutation以外的方式去修改store中state的值。这是因为vuex为了保证状态流转的正确性。当我们提交一个mutation的时候会执行_withCommit把this._committing设置为true。当我们直接store.state.count = 2这样修改state的时候,就会触发上面的错误告警。

js 复制代码
_withCommit(fn) { 
  const committing = this._committing 
  this._committing = true 
  fn() 
  this._committing = committing 
 }

如果再次去执行resetStoreVM。会保存之前的那个store._vm,然后把之前的vm进行销毁。

相关API

当构建好数据仓库以后,vuex给我们提供了一些非常好用的API去操作和管理这个仓库。接下来就来学习一下vuex有哪些API以及它们的实现。

数据存取API

数据获取的两个API: store.state、store.getters。在介绍resetStoreVM过程中已经分析过它们的实现了。

数据存储API

Vuex的数据都存储在store的state上面。对state的修改只能通过定义一些mutations,然后commit一个mutation的时候去执行相应函数然后对state进行修改。mutation也是我们修改state唯一的途径。接下来看一下commit做了什么。

js 复制代码
commit(_type,_payload,_options) { 
  const {type,payload,options} = unifiObjectStyle(_type,_payload,_options)
  const mutation = {type,payload} 
  const entry = this._mutations[type] 
  if(!entry) return 
  this._withCommit(() => { 
    entry.forEach(function commitIterator(handler) { handler(payload) }) 
  }) 
  this._subscribers.forEach(sub => sub(mutation,this.state)) }

通过type从this._mutations去找对应的mutation。调用this._withCommit方法把this._committing设置为true。确保handler执行过程中不会触发警告。执行handler就会去执行上面注册过程声明的函数wrappedMutationHanler。然后执行用户定义的mutation修改state中的数据。mutation是同步的。执行完store.commit('increment')后获取store.state.count值为2。

那异步过程怎么搞?就是通过去定义异步action来实现。action本质上还是commit一个mutation,即修改数据的方式是没有变的。只不过在action执行过程中去执行一些异步操作,比如请求后端接口。等异步操作完以后再通过context.commit去提交mutation。然后再去修改数据。

此时同步输出cout还是1。在then中输出count就是2。通过dispatch派发一个action。

js 复制代码
dispatch(_type,_payload) { 
  const {type,payload} = unifyObjectstyle(_type,_payload) 
  const action = {type,payload} 
  const entry = this._actions[type] 
  if(!entry) return 
  this._actionSubscribers.forEach(sub => sub(action,this.state)) 
  return entry.length > 1 ?
  Promise.all(entry.map(handler => handler(payload))) 
  :entry[0](payload)
 }

通过type拿到action,如果entry.length大于1,执行Promise.all。否则直接执行第一个。

常用语法糖

store是一个原始javascript对象,脱离组件是可以独立运行的。但实际业务中store还是会配合组件进行使用。在这个vuex的注册阶段,是通过mixin的方式,在每个组件的beforeCreate钩子函数注入 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t o r e 指向 s t o r e 实例。这样在组件里面就可以通过 store指向store实例。这样在组件里面就可以通过 </math>store指向store实例。这样在组件里面就可以通过store操作vuex store做一些数据的存取。但是直接操作store不是特别方便,所以vuex提供了更好用的语法糖。其实就是对store的操作再次进行封装,提供更好用的API。

数据获取语法糖

js 复制代码
<div>root state count:{{count}}</div> 
<div> root getter computedCount:{{computedCount}}</div> 
<div> moduleA state count : {{aCount}}</div> 
<div> moduleA getter computedCount:{{aComputedCount}}</div> 
import {mapState,mapGetters} from 'vuex' 
export default { 
  name: 'App', 
  computed: { 
    ...mapState(['count']), 
    ...mapGetters(['computedCount']), 
    ...mapState('a',{aCount: 'count'}),   
    ...mapGetters('a',{aComputedCount:'computedCount'}) 
   } 
 }

通过mapState和mapGetters把数据直接注入到computed里面。这样我们并没有直接去使用$store,使用起来比较方便。接下来分析这两个语法糖的实现源码。

mapState

js 复制代码
const mapState = normalizeNamespace((namespace,states) => { 
  const res = {} 
  normalizeMap(states).forEach(({key,val}) => { 
    res[key] = function mappedState() { 
      let state = this.$store.state 
      let getters = this.$store.getters 
      if(namespace) { 
        const 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] 
       } 
       res[key].vuex = true 
      }) 
      return res 
     }) 
 function normalizeNamespace(fn) { 
   return (namespace,map) => { 
     if(typeof namespace !== 'string') { 
       map = namesapce namespace = ''  
       }elseif(namespace.charAt(namespace.length - 1) !== '/') { 
        namespace += '/'
      } 
      return fn(namespace,map) 
     } 
   }

normalizeNamespace处理参数,然后执行fn函数。然后再执行normalizeMap方法。

js 复制代码
function normalizeMap(map) { 
  return Array.isArray(map) ?map.map(key => ({key,val:key})) 
  :Object.keys(map).map(key => ({key,val: map[key]}))
 }

对于map而言支持数组和对象,如果是数组直接调用数组的map方法,如果是对象就拿keys执行map,举例:

对states做以上处理后开始遍历数组,然后调用 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t o r e 拿到 s t a t e 和 g e t t e r s 。如果没有传入 n a m e s p a c e 且 v a l 不是函数的话直接返回 s t a t e [ v a l ] 。比如 . . . m a p S t a t e ( [ ′ c o u n t ′ ] ) , v a l 就是 c o u n t , 就可以拿到 t h i s . store拿到state和getters。如果没有传入namespace且val不是函数的话直接返回state[val]。比如...mapState(['count']),val就是count,就可以拿到this. </math>store拿到state和getters。如果没有传入namespace且val不是函数的话直接返回state[val]。比如...mapState([′count′]),val就是count,就可以拿到this.store.state.count。如果传入了namespace,会通过getModuleByNamespace的方法拿到module。然后再拿到这个module对应的state和getters。比如...mapState('a',{aCount: 'count'}),就会找到moduleA中state的count值。

mapGetters

mapGetters和mapState非常类似,遍历传入的getters,通过namespace去拼接val。然后去检测是否存在module,如果存在就返回this.$store.getters[val]。比如...mapGetters('a',{aComputedCount:'computedCount'}) 就会找到a/computedCount这个getter。

js 复制代码
const mapGetters = normalizeNamespace((namespace,getters) => { 
  const res = {} 
  normalizeMap(getters).forEach(({key,val}) => { 
    val = namespace + val 
    res[key] = function mappedGetter() { 
      if(namespace && !getModuleByNamespace(this.$store,'mapGetters',namespace)) return 
      return this.$store.getters[val] 
    } 
    res[key].vuex =true 
   }) 
   return res 
  })

数据存储语法糖

js 复制代码
<div @click="increment">root mutation increment</div> 
<div @click="incrementAct"> root action increment</div> 
<div @click="aIncrement"> moduleA mutation increment</div> 
<div @click="aIncrementAct"> moduleA action increment</div> 
import {mapMutations,mapActions} from 'vuex' 
export default { 
  name: 'App', 
  methods: { 
    ...mapMutations(['increment']), 
    ...mapActions({incrementAct:'increment'}), 
    ...mapMutations('a',{aIncrement: 'increment'}), 
    ...mapActions('a',{aIncrementAct: 'increment'}) 
   } 
  }

mapMutations作用就是提交一个increment mutation。mapActions派发一个increment action。对子模块可以添加namespace。本质也是去操作store实例上的方法。

mapMutations

js 复制代码
const mapMutations = normalizeNamespace((namespace,mutations) => { 
  const res = {} normalizeMap(mutations).forEach(({key,val}) => { 
    res[key] = function mappedMutation(...args) { 
      let commit = this.$store.commit 
      if(namespace) { 
      const module =getModuleByNamespace(this.$store,'mapMutations',namespace)
      if(!module) return 
      commit = module.context.commit 
     } 
     return typeof val === 'function' ?
     val.appley(this,[commit].concat(args)) 
     :commit.apply(this.$store,[val].concat(args)) } 
     }) 
    })

如果没有namespace就拿到根模块的commit方法,否则拿对应模块的commit方法,module.context.commit。然后再去执行。

mapActions

js 复制代码
const mapActions = normalizeNamespace((namespace,actions) => { 
  const res = {} 
  normalizeMap(actions).forEach(({key,val}) => { 
    res[key] = function mappedAction(...args) { 
      let dispatch = this.$store.dispatch 
      if(namespace) { 
        const module = getModuleByNamespace(this.$store,'mapAction',namespace)
        if(!module) return 
        dispatch = module.context.dispatch 
       } 
       return typeof val === 'function' ?
       val.appley(this,[dispatch].concat(args)) 
       :dispatch.apply(this.$store,[val].concat(args)) 
      } 
     }) 
    })

mapActions也是类似的过程。不同的是去执行dispatch函数。

模块动态注册过程

js 复制代码
export default { 
  name: 'App', 
  methods: { 
  ...mapMutations(['increment']), 
  ...mapActions({incrementAct:'increment'}), 
  ...mapMutations('a',{aIncrement: 'increment'}), 
  ...mapActions('a',{aIncrementAct: 'increment'}), 
  register() { 
    this.$store.registerModule('c',{ 
      namespaced: true, 
      state: { count: 1 }, 
      mutations: { increment(state) { state.count ++ } } 
    }) 
   } 
  } 
 }
js 复制代码
registerModule(path,rawModule,options={}) { 
  if(typeof path === 'string') 
  path = [path] 
  this._modules.register(path,rawModule) 
  installModule(this,this.state,path,this._modules.get(path),options.preserveState) 
  resetStoreVM(this,this.state) 
 }

调用this._modules.register方法,对_modules做扩展。然后执行installModule注册该模块的actions、mutations、getter等。执行resetStoreVM建立响应式。

Vuex提供这些API都是方便我们在对store做各种操作来完成各种能力,尤其是map***的设计,让我们在使用API的时候更加方便,这也是我们今后在设计一些js库的时候,从API设计角度中应该学习的方向。

插件

Logger插件的实现原理

js 复制代码
import createLogger from 'vue/dist/logger' 
const debug = process.env.NODE_ENV !== 'production' 
const store = new Vuex.Store({ 
  modules: { a: moduleA, b: moduleB }, 
  state: { count: 1 }, 
  actions: { increment(context) { context.commit('increment') } },
  plugins: debug ? [createLogger()] : [] 
 })

配置plugins,createLogger用于在开发环境调试state的变化过程。在实例化store的过程中拿到plugins,遍历执行每一个插件。

js 复制代码
const { 
  plugins = [], 
  strict = false 
 } = options
plugins.forEach(plugin => plugin(this)) // 执行插件的逻辑

对于createLogger插件,返回一个函数。

js 复制代码
function createLogger({ 
  collapsed = true, 
  filter = (mutation,stateBefore,stateAfter) => true 
  transformer = state => state 
  mutationTransformer = mut => mut, 
  logger = console }={}) { 
    return store => { 
      let prevState = deepCopy(store.state) // 保存上一个state
      store.subscribe((mutations,state) => { 
        const nextState = deepCopy(state) 
        if(filter(mutation,prevState,nextState)){ 
        const time = new Date() 
        const formattedTime=`@${pad{time.getHours(),2)}:${pad(time.getMinutes(),2)} :${pad(time.getSeconds(),2)}.${pad(time.getMilliseconds(),3)}`
        const formattedMutation = mutationTransformer(mutation) 
        const messate = `mutation ${mutation.type}${formattedTime}` // collapsed 控制是否要展开console的内容 
        const startMessage = collapsed ? logger.groupCollapsed : logger.group 
        try{ 
        startMessage.call(logger,message) 
        } catch(e) { console.log(message) } // 输出state 
        logger.log(transformer(prevState))
        logger.log(transformer(nextState)) } 
        prevState = nextState }) 
       } 
     }

首先通过deepCopy去深拷贝一份store.state。通过store.subscribe去订阅mutation的变化。实际subscribe的作用就是把函数添加到this._subscribers里面。

js 复制代码
subscribe(fn) { 
  return genericSubscribe(fn,this._subscribers) 
} 
function genericSubscribe(fn,subs) { 
  if(subs.indexOf(fn) < 0) {
    subs.push(fn) 
   } 
   return ()=> { 
     const i = subs.indexOf(fn) 
     if(i > -1) { 
       subs.splice(i,1) 
      } 
     } 
    }

当执行commit的时候会去遍历这个this._subscribers并执行里面的函数:通过deepCopy再次拷贝state。filter返回true即所有的mutation都会记录。这个filter钩子可以让用户改变记录哪些muation。通过logger.log输出prevState和nextState

相关推荐
王哲晓5 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v9 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云19 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v3 小时前
vue经典前端面试题
前端·javascript·vue.js