After a year, and to pick up the VUE source code, ready to record, is also a consolidation of their knowledge.

Vuex initialization

Vuex, as an ecological plug-in of Vue, also supports NPM release. When we import, we execute the code in vuex/dist/vuex.esm.js

export default {
 Store,
 install,
 version: '__VERSION__',
 mapState,
 mapMutations,
 mapGetters,
 mapActions,
 createNamespacedHelpers
}
Copy the code

As you can see, when we import vuex, we actually return an object like this, so we also have the store object on vuex, and when we use vue.use (Vue), we will execute this install. Let’s take a look at the install source. In the SRC/store. Js

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)
}
Copy the code

The logic behind this is that we only execute the applyMixin method once when we call it repeatedly, in SRC /mixin.js

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}}}Copy the code

Vue. Mixin (); mixin (); mixin (); beforeCreate (); mixin (); And then vuexInit, when vuexInit is done, which is essentially injecting our store, we’ll see that we’re going to fetch this.$options, and when options.store exists, check whether store is a function, and if it is, execute it. If it doesn’t, we can assign it directly, if it doesn’t, we can find its parent.$store. This way, we can have a $store object for each vue instance. This allows us to access our store instance from any component through this.$store. So our store was actually passed in when we had new Vue.

const app = new Vue({
    el:'#app',
    store
})
Copy the code

New Vue. Store implementation

In SRC /store.js, we will examine the execution logic of the Store constructor

let Vue // bind on install
export class Store {
    constructor(options ={}){
        if(! Vue && typeof window ! = ='undefined'&& window.vue) {install(window.vue)} /* When we do not use NPM to develop, but use external chain to load Vue,vuex will register the Vue variable on the window and then need to manually execute the install method. * /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 inthis browser.`) assert(this instanceof Store, 'Store must be called with the new operator.')} /* The Vue is first asserted, and this Vue is the one we defined at the top, which is actually assigned when the install method is executed. Vue. Vue (Vuex) is registered before store instantiation. Promises will also be asserted below, because our Vuex entire library relies on promises. If our browser does not support promises natively, we need to make a Promise patch. This is an instance of our Store. When we execute the Store constructor, we must use the new method. If we call the Store function directly, we will raise a warning. */ const { plugins = [], strict =false} = options /* Define some options constants, plugins are supported by VUex */ / store internal state this._research = (case)falsethis._actions = Object.create(null) this._actionSubscribers = [] this._mutations = Object.create(null) This._wrappedgetters = object.create (null) this._modules = new ModuleCollection(options) // Initialize the logic of modules 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)} /* Define a store, cache this, get the dispatch method from this, commit it, re-assign it, when we execute the dispatch, its context is the store. * /}}Copy the code

The first is the new ModuleCollection that initializes modules. The second is installModule, initialize our actions, wrappedGetters,mutations, and resetStoreVM. Now we will focus on the analysis of New ModuleCollection.

New ModuleCollection analysis

In the SRC/module/module – collection. Js

export default class ModuleCollection {
    constructor (rawRootModule) {
        // register root module (Vuex.Store options)
        this.register([], rawRootModule, false} / / Constructor (rawRootModule) constructor (rawRootModule); / / Register (path, constructor); 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)
      })
    }
  }
}
Copy the code

In this case, we pass in our Module definition as a new Module. This new Module, in SRC/Module /module.js, defines a Module class, which I’ll talk about in a moment, which means that the Module becomes an instance. When our path length is 0, we use newModule as the root module, and then determine if we have rawModule.modules. If we do, we go through it and get each corresponding module

Let’s take a look at the installModule implementation

The realization of the installModule

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

So let’s look at what we’re passing, we’re passing in an instance of store, then state, then an empty array of path,

functioninstallModule (store, rootState, path, module, hot) { const isRoot = ! Path. length const namespace = store._modules. GetNamespace (path) /* According to the path.length to determine whether isRoot istrueNamesapce is the method in 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) }) } constlocal = 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)
      })
    }
Copy the code
 getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : ' ')},' ')}Copy the code

The namespace is constructed through path.reduce, and the namespace in turn finds its child modules through module.getChild. During the search, we do a concatenation of the value when module.namespaced is true. Then get the corresponding namespace to do the corresponding assignment.

So now we’re going to define a local, and the key is the makeLocalContext function, so let’s see what it’s basically doing.

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
}
Copy the code

This function finally returns a local object, which redefines dispatch and commit in local. At the beginning of the function, the namespace is assigned to noNamespace to determine whether it is empty. When noNamespace is empty, It’s actually our store.dispatch. An important point is that we concatenate our type to namespace and reassign it to type. That’s why when we use it, we see it’s a patchwork effect. Commit does some parameter processing before concatenating a namespace. Getters does the same thing. So that’s our implementation of localContext. The returned local will be in the following four forEach… The function is going to reference it.

So let’s analyze these four functions. 1. First, take a look at the mutation registration process, which actually executes the module

forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
Copy the code

See if there is mutations under the module we define, if there is, we will go through. After traversal, the registerMutation function is executed to perform a registration.

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)
  })
}
Copy the code

What you actually create is an array corresponding to _mutations, and _mutations[type] is an empty array if it doesn’t exist. And then we push the wrappedMutationHandler into this array, and then when the wrappedMutationHandler executes it will execute the handler, and you can see that when handler. Call, store is in this context, And then local.state, so in

2. Then look at registerAction. It is actually similar to mutation. We see that the action has an action. Root configuration. If there is one, you do not need to concatenate the namespace, otherwise you still need to concatenate the namespace. Then go and register the 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
    }
  })
}
Copy the code

We see that when handler.call is executed, the context has a lot of parameters, which is what the official website mentions

3. The next step is to look at the 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
    )
  }
}
Copy the code

RegisterGetter is a little different from the others. Getters is not an array but a function, wrappedGetter. It returns a rawGetter.

Mutation (action, getters, and mutation) And the whole thing is essentially building a tree barn. And then when we do data processing, we can clearly know how to deal with it.

The realization of the resetStoreVM

ResetStoreVM (this, state) at this point we pass this._modules.root.state as an argument.

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()}}Copy the code

We first define a public getters for our store, and then run this command to get the store. _wrappedgeters. Then run this through wrappedGetters, store._vm=new Vue, and this one is doing a response using Vue, passing in data and computed, right? State =state, which is a parameter passed in, which is why we access module.state to access store.state, and when we access each getters, Returns a store._vm[key] that returns the calculation result of fn(Store) through the calculation property. And inside of that is the dependency between the state and the getter. Finally, when we execute resetStoreVm again, we will take the previous store.vm and keep it, destroy it, and then rebuild its store.vm.

This is the whole instantiation process. Store is a data warehouse. For better management, we split a large store into modules, and the modules are a tree structure. Each module defines the state respectively, getters, mutations, actions, and then by means of recursive traversal module completed their initialization. That’s when our store is set up.

Let’s take a look at some grammatical sugar

MapState implementation

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
})
Copy the code

From this code we see that the return value of mapState is the result of the normalizeNamespace function. Now let’s look at the delta of this function

 /**
   * 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) } }Copy the code

If no namespace value is passed, assign the namespace value to map. Otherwise, if both parameters are present, the last bit of the namespace is not one bit. It automatically adds a ‘/’, which it is

export default {
    name:'App', computed:{ ... mapState(['count'
        ]),
        ...mapState('a',{
            aCount:'count'}}})Copy the code

It doesn’t matter if you put a slash after the a. In fact, this function mainly deals with namespace and map. Finally, we execute fn, at which point we look at the mapState code that we posted in the beginning. In the function, normalizeMap is executed, passing in the States

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
Copy the code

This function, in the case of map, supports two types, one is an array, the other is an object, both of which return a key or value. If you are an array, call the array map method directly, for example:

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

If it is an object, use the keys of the object, for example:

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

The normalizeMap function iterates through the result. If the namespace does not have a value, it evaluates the val value. If the namespace does not have a value, it returns the value. Get the module value using the getModuleBynamespace method, which is simple

 function getModuleByNamespace (store, helper, namespace) {
    var module = store._modulesNamespaceMap[namespace];
    if(! module) { console.error(("[vuex] module namespace not found in " + helper + "()." + namespace));
    }
    return module
  }
Copy the code

For example, during initialization, this Module will be constructed.

Module.context. state and module.context.getters, which are local, will be returned when the module is found. Js, in vuex/SRC/store.

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

And in the makeLocalContext,

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

Copy the code

It also defines local getters and state, which means we can access local data,

That’s what’s under module A, so this aCount is going to be the aCount under module A. So that’s what we do with mapState.

mapGetters

MapGetters is very similar to mapState, and is implemented through normalizeNamespace. We pass our getters to normalizeMap and iterate over the return value. Val can also be concatenated.

mapMutations

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = functionmappedMutation (... args) { // Get the commit method from storelet 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
})
Copy the code

We can see that the code of mapMutations is very similar to that of mapState. When implementing this function, mutations will normalizeMap once and change into the form of key and val, and determine whether namespace exists. If not, Commit is our $store.mit, and eventually call the commit method directly, if there is one, Go to the getModuleNamespace method to find the corresponding module, then find the module.context, the local context to find the corresponding commit method, and then commit.

mapActions

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = functionmappedAction (... args) { // get dispatchfunction 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
})
Copy the code

The method for mapActions is the same, but with dispatch. The corresponding dispatch is found, and the corresponding dispatch is executed.

These are what we call syntactic sugar, enhancements to the original syntax that allow us to do the same thing with less code. This is also let us learn a point, the future in the design of some JS libraries, from the perspective of API design should learn the direction.