vuex源码分析

时隔一年了,又从新把vue源码拿起来了,准备记录下来,也算是对本身知识的一个巩固。vue

Vuex初始化

vuex做为vue的一个生态插件,也是支持npm发布的,当咱们import的时候呢,执行的是vuex/dist/vuex.esm.js这里面的代码 vuex

那它是肿么打包的呢,咱们看下build/config.js中

咱们看一下这个入口文件,

export default {
 Store,
 install,
 version: '__VERSION__',
 mapState,
 mapMutations,
 mapGetters,
 mapActions,
 createNamespacedHelpers
}
复制代码

能够知道当咱们import vuex的时候,实际上就会返回这样的一个对象,因此咱们vuex上也有store这个对象,当咱们使用Vue.use(Vue)的时候,就会执行这个install。咱们来看下这个install的源码。在src/store.js中npm

export function install (_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方法,在src/mixin.js中api

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const 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
    }
  }
}
复制代码

首先先会对咱们的vue版本进行一个判断,1.x和2.x走的都是这一套代码,只是执行的方法不同,2.x呢我看能看到是执行的Vue.mixin方法,混入beforeCreate这个钩子函数,而后混入了vuexInit,在vuexInit的时候呢,其实就是把咱们的store注入进来,咱们会看到先去取这个this.$options,当options.store存在的时候呢,判断store是不是函数,若是是函数就去执行,若是不是就直接赋值,若是没有的话,就去找它的parent.$store,经过这种方式,能让咱们每个vue实例都有一个$store对象。这样咱们能够在任意的组件中经过this.$store能够访问到咱们的store实例。那么咱们这个store实际上是在new Vue的时候传入的。数组

const app = new Vue({
    el:'#app',
    store
})
复制代码

new Vue.Store实现

对于Store的定义呢是在src/store.js中,咱们来分析一下store的这个构造函数的执行逻辑promise

let Vue // bind on install
export class Store {
    constructor(options ={}){
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        /*
        当咱们不经过npm的方法去开发,面是经过外链的方式去加载vue,vuex,会在window上注册Vue这个变量,而后须要手动去执行install方法。
        */
        if (process.env.NODE_ENV !== 'production') {
            assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
            assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
            assert(this instanceof Store, `Store must be called with the new operator.`)
        }
        /*
        这几行呢就是若是是在非生产环境下有这么几个断言,首先会断言Vue,这个Vue呢就是最头上咱们定义的这个Vue,这个Vue其实在执行install方法的时候就会赋值。断言的意义呢就是执行store实例化前呢,咱们必定要经过Vue.vue(Vuex)去注册,注册完了才能实例化。
        下面也会对Promise进行断言,由于咱们Vuex整个库是依赖promise,若是咱们的浏览器没有原生支持promise,须要打一个promise的一个补丁。
        最后一个是判断this是咱们Store的一个实例,咱们去执行Store的构造函数的时候,必须是经过new的方式,若是直接调用store的函数就会报出警告。
        */
        const {
          plugins = [],
          strict = false
        } = options
        /*
            定义了一些options的常量,plugins是vuex支持的一些个插件
        */
        // store internal state
        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) //初始化modules的逻辑
        this._modulesNamespaceMap = Object.create(null)
        this._subscribers = []
        this._watcherVM = new Vue() 
        /*这些就是在store上定义了一些私有的属性*/
        
        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)
        }
        /*
        定义了一个store,缓存了this,而后经过这个this拿到了dispatch,commit方法,而后从新给它赋值,当咱们在执行dispatch的时候,它的上下文就是这个store。
        */
    }
}
复制代码

初始化过程当中呢,重点有三个部分,第一个就是new ModuleCollection去初始化这个modules.第二个就是installModule,初始化咱们这些个actions呀,wrappedGetters,mutations呀,还有就是去执行resetStoreVM。 下面咱们来重点分析下new ModuleCollection.浏览器

new ModuleCollection 分析

在src/module/module-collection.js中缓存

export default class ModuleCollection {
    constructor (rawRootModule) {
        // register root module (Vuex.Store options)
        this.register([], rawRootModule, false)
    }
    /*咱们在执行new Module的时候呢就去执行这个constructor,而后将rawRootModule做为参数传入,这个就是咱们外面定义的module,而后去执行register方法*/
    
    register (path, rawModule, runtime = true) {
        if (process.env.NODE_ENV !== 'production') {
        assertRawModule(path, rawModule)
    }
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }
    
    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}
复制代码

这里面呢,咱们new Module的方式将咱们的module定义传入,这个new Module,在src/module/module.js中,定义了一个Module的类,稍后我会讲这个,也就是说这个module转成了一个实例。 当咱们的path长度为0的时候,就将newModule做为根module,而后判断咱们是否有这个rawModule.modules,若是有的话就去遍历这个,拿到每个对应的module,上个图瞅一眼bash

而后会再次执行this.register这个方法,递归调用。注意一点这个时候path就会有了新的变化,path.concat(key),这个key就是module前面定义的这个key值,也就是上图中的a,b。 咱们再看当path长度不为0的时候,就是创建父子关系的一个过程。 经过register这个逻辑呢就是将module创建一个树状结构。这就是执行了new ModuleCollection的一个返回结果。看一下结构图。

下面咱们分析下installModule的实现markdown

installModule 的实现

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

先看下要传的值,把store的实例传入,而后是state,而后是path为空数组,

function installModule (store, rootState, path, module, hot) {
    const isRoot = !path.length
    const namespace = store._modules.getNamespace(path)
    /*
    根据这个path.length来判断isRoot是否为true,namesapce呢就是module-collection.js中的方法
    */
    // set state
     if (!isRoot && !hot) {
        const parentState = getNestedState(rootState, path.slice(0, -1))
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
          Vue.set(parentState, moduleName, module.state)
        })
    }
    const local = module.context = makeLocalContext(store, namespace, path)
    module.forEachMutation((mutation, key) => {
        const namespacedType = namespace + key
        registerMutation(store, namespacedType, mutation, local)
     })
    
    module.forEachAction((action, key) => {
        const type = action.root ? key : namespace + key
        const handler = action.handler || action
        registerAction(store, type, handler, local)
    })
    
    module.forEachGetter((getter, key) => {
        const namespacedType = namespace + key
        registerGetter(store, namespacedType, getter, local)
    })
    
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child, hot)
      })
    }
复制代码
getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }
复制代码

经过path.reduce构造这个namespace,而后namespace又是经过module.getChild去一层层找它的子module。在找的过程当中,module.namespaced为true的状况下,对这个值进行一个拼接。而后拿到对应的namespace去作对应的赋值。

就是上图示例的这个过程。 咱们继续往下分析。

下面给咱们定义了一个local,关键点在于makeLocalContext函数,咱们来看下它主要作了些什么。

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
        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)
    },
    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
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }
      store.commit(type, payload, options)
    }
  }
  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
复制代码

这个函数最终返回的是一个local对象,在local里面从新定义了dispatch,commit,在函数开始将namespace判断是否为空赋值给了noNamespace,当noNamespace为空的时候,实际上就是咱们的store.dispatch,下面有一个重要的点就是将咱们的type拼接给了namespace从新赋值给type,拼接完后才会调用store.dispatch。这就是为何咱们在使用的时候,会看到它是拼接的效果。commit也是同样,先是作一些参数的处理,而后再去拼接namespace,getters也是这样的思路。这个就是咱们localContext的实现。返回来的local呢会在下面四个forEach...函数会去引用。

下面来分析下这四个函数。 1.首先来看下mutation的一个注册过程,它实际上会去执行module.js中的

forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
复制代码

看咱们定义的module下面有没有mutations,若是有的话就会去遍历。遍历完后会去执行registerMutation函数,进行一个注册。

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

实际上建立的是一个_mutations对应的数组,_mutations[type]若是不存在,就是一个空数组。而后将wrappedMutationHandler push到这个数组中,而后这个wrappedMutationHandler执行的时候会去执行handler,能够看到handler.call的时候store是这个上下文,而后是local.state,因此在

图中的state,就是咱们的这个local.state。因此这就是为何咱们提交一个mutation的时候state就是咱们这个local.state。

2.而后就是来看registerAction。 它其实和mutation是相似的。咱们看到action有一个配置是action.root,若是存在不用去拼接namespace,不然仍是须要去拼接。而后去注册registerAction

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)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
复制代码

咱们看到在执行handler.call的时候,对应的context有不少参数,这也就是官网提到的

包含如下属性的缘由。

3.其次就是来看registerGetter。

function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${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
    )
  }
}
复制代码

registerGetter和其余的就有些不同之处了。getters对应的就不是一个数组了而是一个函数,wrappedGetter。返回的是一个rawGetter。

以上呢咱们就知道了如何把action,getter,mutation进行一个注册。其实整个呢其实就是构造了一个树仓。以后再进行数据处理的时候咱们就能清晰的知道如何去对应的处理。

resetStoreVM 的实现

resetStoreVM(this, state) 这个时候咱们会把this._modules.root.state做为参数进行传入。

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {} 
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
复制代码

首先给咱们store定义了一个public getters,而后根据store._wrappedGetters拿到去计算拿到这个store.getters。而后进行遍历这个wrappedGetters,下面呢store._vm=new Vue,这个呢是利用vue作一个响应式,里面传入了data和computed,?state=state,后者state是传入的一个参数,这就是咱们访问module.state就能访问到store.state,当咱们访问每个getters的时候,返回一个store._vm[key],经过计算属性返回fn(store)的计算结果。在这里面呢创建了state和getter的依赖关系。 最后呢就是说当咱们再次去执行这个resetStoreVm,咱们会将把以前的store.vm拿到进行保留,而后将以前的进行销毁,而后再从新创建它的store.vm。

以上是整个实例化的一个过程。store呢就是一个数据仓库,为了更好的管理呢,咱们将一个大的store拆成了一些modules,整个modules是一个树形结构。每一个module又分别定义了state,getters,mutations,actions,而后经过递归遍历模块的方式完成了它们的初始化。这个时候咱们的store就创建起来了。

咱们来看一下几个语法糖

mapState实现

export 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]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
复制代码

从这段代码中咱们看到mapState 的返回值是经过normalizeNamespace函数执行的结果。 如今咱们看一下这个函数的

/**
   * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. * @param {Function} fn * @return {Function} */ function normalizeNamespace (fn) { return function (namespace, map) { if (typeof namespace !== 'string') { map = namespace; namespace = ''; } else if (namespace.charAt(namespace.length - 1) !== '/') { namespace += '/'; } return fn(namespace, map) } } 复制代码

咱们能够看到return中包含两个值,一个是namespace一个是map, 判断若是没有传namespace值,就把namespace赋值给map,不然两个参数都有的状况下,那么,namespace的最后一位若是不是一位的话,就自动添加一个'/', 实际上呢就是

export default {
    name:'App',
    computed:{
        ...mapState([
            'count'
        ]),
        ...mapState('a',{
            aCount:'count'
        })
    }
}
复制代码

这里面的a后面加不加'/'都无所谓了。 其实这个函数主要是对namespace和map作一下处理。最后执行一个fn,这个时候咱们去看一开始贴入的mapState的代码。 在函数中呢,执行了一个normalizeMap,把states传入,这个states呢就是

这个值, 咱们来看下normalizeMap作了些什么操做

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

这个函数也就是map而言,支持两种类型,一种是数组,一种是对象,都是返回个key,value的形式。若是你是数组的话,就直接调用数组的map方法,例如:

normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ],

若是是一个对象呢,就用对象的keys,例如:

normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]

normalizeMap函数处理完以后对结果进行遍历,若是namespace没有值的状况下,对val值进行一个判断,若是不是一个函数,就直接把值返回去,若是namespace有值的状况下,根据getModuleBynamespace这个方法去拿到这个module的值,这个方法很简单

function getModuleByNamespace (store, helper, namespace) {
    var module = store._modulesNamespaceMap[namespace];
    if (!module) {
      console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace));
    }
    return module
  }
复制代码

根据namespace,经过store._modulesNamespaceMap去拿到,举个示例:在初始化阶段呢,就会构造这个Module,

找到这个module就会进行返回,这个时候state和getters的值就是会module.context.state和module.context.getters, 也就是咱们这个local,咱们能够去看下这个local的源码,在vuex/src/store.js中,

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

在makeLocalContext函数中呢,

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

复制代码

也定义了local的getters和state,也就是说咱们能够访问到local中的数据了,

也就是a模块下的内容,这个aCount也就是对应到模块A下的aCount。 以上呢就是咱们mapState所作的事情。

mapGetters

mapGetters其实和mapState很是的相似,也是经过normalizeNamespace函数来执行,将咱们的getters传入到normalizeMap中将返回值进行遍历,val值也是能够是拼接出来的,

而后res[key]对应的每个key的值也就是一个mappedGetter函数,先去检测namespace是否存在,存在就直接执行,返回this.$store.getters[val],这个val值为拼接后的,因此咱们能够找到对应的子模块下的值。例如:
对应的就是store中的getters下的a的computedCount
这个呢就是咱们mapGetters作的一些事情,因为和mapState有一些类似之处,就再也不细致写了。

mapMutations

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      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.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})
复制代码

咱们能够看一下,mapMutations的这段代码和mapState的很类似,在执行这个函数的时候会把mutations作一次normalizeMap,变成这个key,val的形式,也会去判断namespace是否存在,若是没有的话,commit就是咱们这个$store.commit,最终去直接调用commit的方法,若是有的话,就去getModuleNamespace方法去找到对应的module,而后找到module.context,局部的上下文找到对应的commit方法,再去提交。

mapActions

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const 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
})
复制代码

mapActions的这个方法呢也是同样的,只不过换成了dispatch。找到了对应的dispatch,执行相应的dispatch。

这些呢就是咱们所谓的语法糖,对原有语法的一些加强,让咱们用更少的代码去实现一样的功能。这也是让咱们学习到了一点,从此在设计一些js库的时候,从api设计角度中应该学习的方向。

相关文章
相关标签/搜索