preface

I think everyone may have had the idea of looking at the source code, including me. Because look at the source code can not only make yourself more familiar with the library, but also to learn the author’s strong thoughts, over time, their level and thought will have a significant improvement.

But for me, before never read the source code, want to read the source code but dare not take that step, because a mature library has too many methods, logic, reading may be more difficult, but people always have the courage to try, so I was ready to Vuex source clone down, no other reason, Just because the library is relatively small, with less than 1000 lines of core code including comments, I find it ideal for first-time source readers

Immediately, I made a schedule for myself on Github. I expected to finish the source code in 15 days and complete the summary, and then record the harvest of the day every day

But the final result is out of my expectation, read the source code plus collation summary only 8 days or so of time

Before reading the source code, I first went to look at the Vuex official documentation, which is a kind of review, check the leak to fill the gap, I also highly recommend doing this, because you look at the source code, you will see all the contents of the library, then you even don’t understand the library, the difficulty of reading the source code has increased invisible! How much easier is it to understand the source code by becoming familiar with the methods used in the library (even if you don’t know why) and then thinking about the methods used when you see the corresponding code as you read it

Here put the Vuex official document link, if interested to follow my thoughts to read the Vuex source partners can first put all the use mentioned in the document are familiar with ➡️ Vuex official documentation

There is a summary and q&A session at the end

🔥 source code parsing

For all the comments and understanding of the source code I have included in my Github vuex-Analysis warehouse, want to see more detailed comments, you can fork down the reference ➡️ Vuex source code parsing warehouse address link (feel good can click a star support)

Next this article in accordance with the thinking I was reading the source code, step by step to explain in detail, I hope you patience to read, thank you ~

First, source directory structure analysis

The entire Vuex source file is very much, we directly look at the main file, namely the contents of the SRC folder, the structure of the example is as follows:

├ ─ ─ the SRC ├ ─ ─module    // Module related operations│ ├ ─ ─module-collection.js   // To collect and register root and nested modules│ └ ─ ─module.js   // Define the Module class, which stores the information in the Module, such as: state...│ ├ ─ ─ the plugins// Some plugins│ ├ ─ ─ devtool. Js// Develop a debug plugin│ └ ─ ─ logger. Js// │ ├ ─ ─ helpers. JsMutations: mapState, mapGetters, mapMutations...├ ─ ─ index. The CJS. Js// Commonjs packages the entry├ ─ ─ index. Js// Import file├ ─ ─ index. MJS// Es6 Module package entry├ ─ ─ a mixin. Js// Mount the vuex instance to the $store of global Vue├ ─ ─ store. Js// The core file that defines the Store class└ ─ ─ util. Js// Provide utility functions such as deepCopy, isPromise, isObject...
Copy the code

Two, source reading

1. View utility functions

The first thing I thought I would do is take a look at util.js, which is where the utility functions are frequently used in the source code, so I thought it would be a good idea to first understand what each function does

/**
 * Get the first item that pass the test
 * by second argument function
 *
 * @param {Array} list
 * @param {Function} f
 * @return {*}* /

// Find the first element in the array list that meets the requirements
export function find (list, f) {
  return list.filter(f)[0]}/** deep copy **@param {*} obj
 * @param {Array<Object>} cache
 * @return {*}* /
export function deepCopy (obj, cache = []) {
  // just return if obj is immutable value
  if (obj === null || typeofobj ! = ='object') {
    return obj
  }

  // if obj is hit, it is in circular structure
  const hit = find(cache, c= > c.original === obj)
  if (hit) {
    return hit.copy
  }

  const copy = Array.isArray(obj) ? [] : {}
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push({
    original: obj,
    copy
  })

  Object.keys(obj).forEach(key= > {
    copy[key] = deepCopy(obj[key], cache)
  })

  return copy
}

// Iterates through the value of each property of the obj object
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key= > fn(obj[key], key))
}

// Check whether it is an object (null excluded)
export function isObject (obj) {
  returnobj ! = =null && typeof obj === 'object'
}

// Check whether it is a Promise object
export function isPromise (val) {
  return val && typeof val.then === 'function'
}

/ / assertions
export function assert (condition, msg) {
  if(! condition)throw new Error(`[vuex] ${msg}`)}// Retain the closure of the original arguments
export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}
Copy the code

I’ve commented out what each function does, so a little reading should show you what it does

2. Import the file

Most of the code is in the SRC directory, so the following files are in the default SRC directory

The first step is to start with the entry file index.js, but there are also index. CJS and index. MJS, which are the packaged entry for commonJS and ES6 Modules, respectively

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'

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

export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}
Copy the code

As can be seen from the entry file, the Store class, install method and some auxiliary functions (mapState, mapMutations, mapGetters…) are mainly exported.

So we’ll focus on the core vuex code, which is store.js, and you can see that the store class comes from this file

3. Implementation of the Store class

The main logic of the entire Store class is in its constructor, so let’s take a look at the logic and code from this constructor

3.1 Storing class status

First, it defines some instance states for storing modules, mutations, actions, getters caches, and so on

const {
  plugins = [],
  strict = false
} = options      // Generate the incoming arguments for the Store class

this._committing = false        // Represents the submitted state. When the state is changed by mutations, the state is true, and after the state value changes, the state becomes false; In strict mode, it listens for the change of state value, and when it changes, and _research is false, it warns, indicating that the change of state value isn't due to mutations

this._actions = Object.create(null)  // Record the names of all existing actions methods (both global and namespace, with repeated definitions allowed)

this._actionSubscribers = []       // Store the callbacks subscribed to by the actions method

this._mutations = Object.create(null)  // To record all existing mutations method names (both global and namespace, allowing for repeated definitions)

this._wrappedGetters = Object.create(null)  // Collect all module wrapped getters (including global and namespace, but not allowed to duplicate definitions)

this._modules = new ModuleCollection(options)  // Each module is registered according to the incoming options configuration. At this time, only the relationship between each module has been registered and established, and the state state of each module has been defined, but getters, Mutations and other methods have not been registered yet

this._modulesNamespaceMap = Object.create(null)   // Stores the module that defines the namespace

this._subscribers = []    // Deposit the callbacks for the mutations method subscription

this._watcherVM = new Vue()  // To listen for state, getters

this._makeLocalGettersCache = Object.create(null)   // Local cache of getters
Copy the code

This._modules = new ModuleCollection(option) is used to collect modules recursively. According to the source of ModuleCollection, Let’s move to the./module/module-collection.js file

3.1.1 Recursive collection module

The ModuleCollection class is defined in the modul -collection.js file. The ModuleCollection class generates a separate Moudle for each Module by recursively traversing the options input

The options structure is as follows:

import Vuex from 'vuex'

const options = {
  state: {... },getters: {... },mutations: {... },actions: {... },modules: {
    ModuleA: {
      state: {...},
      ...
      modules: {
        ModuleA1: {... }}},ModuleB: {
      state: {...},
      ...
      modules: {
        ModuleB1: {... } } } } }const store = new Vuex.Store(options)

export default store
Copy the code

You can see options passed in as a root module, and then root’s modules are nested within two other submodules: ModuleA and ModuleB, and ModuleA and ModuleB are also nested within a submodule, ModuleA1 and ModuleB1 respectively. This makes up a Module tree, so the ModuleCollection class’s job is to preserve the original Module relationship, encapsulating each Module into a Module class

export default class ModuleCollection {
  constructor (rawRootModule) {
    // Register the module recursively
    this.register([], rawRootModule, false)}// Recursively get from the root module to the parent module to which we are going to add new modules
  get (path) {
    return path.reduce((module, key) = > {
      return module.getChild(key)
    }, this.root)
  }
  
  // Register the module recursively
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }
    
    const newModule = new Module(rawModule, runtime)  // Initialize a new module
    if (path.length === 0) {    // There are no other modules
      this.root = newModule     // This module is the root module
    } else {    // There are multiple modules
      const parent = this.get(path.slice(0, -1))   // Get the parent module that the new module is dependent on, so path.slice(0, -1), and the last element is the name of the submodule we want to add
      parent.addChild(path[path.length - 1], newModule)    // Add a new submodule to the parent module
    }

    if (rawModule.modules) {     // If there are nested modules
      /** * 1. * 2. Store the names of all submodules except the root module * */ in path 
      forEachValue(rawModule.modules, (rawChildModule, key) = > {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}
Copy the code

Function action:

  1. register(path, rawModule, runtime): Registers a new module and adds the new module as a submodule of the corresponding module according to the module nesting relationship
  • Path: indicates the module nesting relationship. There is no nesting relationship when the current module is the root modulepath = []; If the current module is not the root module, there is a nesting relationship, as in the above exampleModuleA1, it isModuleASubmodule, andModuleAIs a submodule of the root modulepath = ['ModuleA', 'ModuleA1']
  • RawModule: indicates a module object. In this case, it is an object type
  • Runtime: Indicates the runtime of the program
  1. Get (path) : Gets the Module class we want based on the path passed in

The constructor of ModuleCollection calls the register function. The first two arguments are: [] and rawRootModule. In this case, the registration must start from the root module, so there is no content in path, and rawRootModule refers to the root module

Then look at the logic in the register function.

  1. First, generate a Module from the Module to be registered, and take rawModule as the parameter to store the Module information

  2. If (path.length === 0) is the root Module; if(path.length === 0) is the root Module; Otherwise, skip to step 3

  3. To determine that the current module is not the root module, we use the get function to find the parent module of the current module, and then call the addChild method in the parent module to add the current module to the child module

  4. Finally, determine whether there are nested modules in the current module, if there are, go back to step 1 for recursive operation; Otherwise, do nothing

Following the logic above, you can recursively collect and register all modules. There is one Module class that is not mentioned yet, so you can move on to./ Module /module.js

import { forEachValue } from '.. /util'

Mutations, getters, Actions, and Modules are defined in Vuex
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    
    this._children = Object.create(null)   // Create an empty object to hold the submodules of the current module
    
    this._rawModule = rawModule         Mutations, getters, Actions, and Modules
    const rawState = rawModule.state    Function type => return an obj object; 2. Obtain the obj object directly

    // Stores the state of the current module
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}   
  }

  // If namespaced is defined, return true; Otherwise return false
  get namespaced () {
    return!!!!!this._rawModule.namespaced
  }

  // Add a submodule named key
  addChild (key, module) {
    this._children[key] = module
  }

  // Remove the submodule named key
  removeChild (key) {
    delete this._children[key]
  }

  // Get the submodule named key
  getChild (key) {
    return this._children[key]
  }

  // Whether there is a submodule named key
  hasChild (key) {
    return key in this._children
  }
	
  // Update the current module's namespace to the specified module's namespace, and update the call sources for Actions, Mutations, getters
  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  // Iterate through all submodules of the current module and perform the callback operation
  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  // Iterate over all getters in the current module and perform the callback operation
  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  // Iterates through all actions for the current module and performs a callback
  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  Traverse all mutations for the current module and perform a callback operation
  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}
Copy the code

Take a look at what was done inside the Module class created during Module collection, again starting with Constructor

This._children is an object value that holds the other Module classes nested within this Module;

This._rawModule is used to store some information inside the module, such as state, mutations, actions, getters, moudles;

This. State corresponds to the state in this._rawModule;

That’s what happens in the constructor, and we can see that when we generate a Module class, it only defines the state property, but mutations, getters, actions, modules are not defined, For example, all the solutions of the Module cannot be obtained through Module. Mutations, so when were these solutions defined? Of course, this is done after all the modules are collected, because nested modules in VUex will probably have the namespaced namespace

3.2 Registration Module

At this point, the classes for each module have been created, so continue back to the constructor constructor of./ SRC /store.js

// Bind the dispatch and commit methods to an instance of the Store to avoid changing the point of this when you use dispatch or commit later
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)
}

// Check whether the store is not in strict mode. True: All states must be changed by mutations
this.strict = strict

// Assign the root module's state to the state variable
const state = this._modules.root.state
Copy the code

This code first wraps the Dispatch and Commit methods on the Store instance by referring them to the current Store instance with a call. This prevents subsequent operations, This.$store.dispatch.call(obj, 1

This.strict is used to determine if the mode is strict. Because in VUEX, it is suggested that all changes of state variables must go through mutations, because only in this way can they be recorded by DevTool. Therefore, in strict mode, if the value of state is directly changed without mutations, a warning ⚠️ will be issued in the development environment

Const state = this._modules.root.state gets the state of the root module, which is used for some subsequent operations

With everything in place, it’s time to register the information for each module

// Start with the root module and recursively refine the information for each module
installModule(this, state, [], this._modules.root)
Copy the code

The installModule method is called and passed as arguments the Store instance object, the state property, the path, and the root module object

// Register to improve the information in each module
function installModule (store, rootState, path, module, hot) {
  constisRoot = ! path.length// Whether it is the root module
  const namespace = store._modules.getNamespace(path)  // Get the namespace of the current module in the format of second/ or second/third/

  // If the current module is set to Namespaced or inherits namespaced from its parent module, then store the current module in the modulesNamespaceMap
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // If not the root module, register the current module's state with the parent module's state
  if(! isRoot && ! hot) {const parentState = getNestedState(rootState, path.slice(0, -1)) // Obtain the state of the parent module
    const moduleName = path[path.length - 1]   // The name of the current module
    store._withCommit(() = > {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('. ')}"`)}}// Register the current module's state with the parent module's state, and be responsive
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // Set the current module context
  const local = module.context = makeLocalContext(store, namespace, path)

  // Register all mutations of the module
  module.forEachMutation((mutation, key) = > {
    const namespacedType = namespace + key     // Example: first/second/mutations1
    registerMutation(store, namespacedType, mutation, local)
  })

  // Register all actions for the module
  module.forEachAction((action, key) = > {
    /** * actions: {* AsyncAdd (context, payload) {... }, // * AsyncDelete: {// * root: true, * handler: (context, payload) {... } *} *} */
    const type = action.root ? key : namespace + key   // Determine whether to register a global action in the namespace
    const handler = action.handler || action          // Get the function corresponding to the actions
    registerAction(store, type, handler, local)   
  })

  // Register all getters for the module
  module.forEachGetter((getter, key) = > {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // Register submodules recursively
  module.forEachChild((child, key) = > {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
Copy the code

Const namespace = store._modules.getNamespace(path) Call the getNamespace method on the ModuleCollection class instance to get the namespace of the currently registered object

/** * set A path name based on whether the module has A namespace * for example, A is A parent module, B is A submodule, and C is A descendant module * 1. If the namespace of module B is second and the namespace of module C is not set; Module C inherits module B's namespace, which is second/ * 2. If module B has no namespace, module B's namespace is third. Then module B inherits the namespace of module A, while module C inherits the namespace path of third/ */
getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) = > {
    module = module.getChild(key)   // Get the submodule
    return namespace + (module.namespaced ? key + '/' : ' ')},' ')}Copy the code

As you can see, a module that does not specify a namespace inherits the parent module’s namespace

  // If the current module is set to Namespaced or inherits namespaced from its parent module, then store the current module in the modulesNamespaceMap
if (module.namespaced) {
  if (store._modulesNamespaceMap[namespace] && __DEV__) {
    console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
  }
  store._modulesNamespaceMap[namespace] = module
}
Copy the code

This code records all namespacemap modules in the store._modulesNamespacemap so that later helper functions can be called. (I haven’t mentioned helper functions yet, but I’ll come back to it later.)

3.2.1 Registering the state of a module
// If not the root module, register the current module's state with the parent module's state
if(! isRoot && ! hot) {const parentState = getNestedState(rootState, path.slice(0, -1)) // Obtain the state of the parent module
  const moduleName = path[path.length - 1]   // The name of the current module
  store._withCommit(() = > {
    if (__DEV__) {
      if (moduleName in parentState) {
        console.warn(
          `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('. ')}"`)}}// Register the current module's state with the parent module's state, and be responsive
    Vue.set(parentState, moduleName, module.state)
  })
}
Copy the code

This code basically mounts the non-root module’s state onto the parent module’s state

Slice (0, -1)) const parentState = getNestedState(rootState, path.slice(0, -1)) Take a look inside the getNestedState method

// Get the state in the nested module
function getNestedState (state, path) {
  return path.reduce((state, key) = > state[key], state)
}
Copy the code

Const moduleName = path[path.length-1] Extract the name of the current module from the path

store._withCommit(() = > {
  if (__DEV__) {
    if (moduleName in parentState) {
      console.warn(
        `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('. ')}"`)}}// Register the current module's state with the parent module's state, and be responsive
  Vue.set(parentState, moduleName, module.state)
})
Copy the code

The main part of this code is vue.set (parentState, moduleName, module.state), which calls Vue’s set method to add the current module’s state responsively to the parent module’s state. This is because later we will see that the state will be placed in the data of a new Vue instance, so we have to add it in a responsive way using Vue’s set method

$store.state.modulea.name = this.$store.state.modulea.name = this.$store.state.modulea.name = this.$store.state.modulea.name

3.2.2 Generate the module invocation context
// Set the current module context
const local = module.context = makeLocalContext(store, namespace, path)
Copy the code

This line of code is also a very core piece of code. It creates a context for each module based on its namespace and assigns the context to the context property given to the module

Now how is this context created

// Create a local commit and dispatch method if namespace is set, otherwise use a global store
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) {// If root:true is passed as the third parameter, the global actions method is dispatched
        type = namespace + type
        if(__DEV__ && ! 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) {// If the third parameter is passed in and root:true is set, the solution is delivered for global mutations
        type = namespace + type
        if(__DEV__ && ! store._mutations[type]) {console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

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

  /** * If the namespace is not set, the system reads store.getters (store.getters has been mounted on computed in the Vue instance). If namespace is set, getters */ is read from the local cache _makeLocalGettersCache
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () = > store.getters    
        : () = > makeLocalGetters(store, namespace)
    },
    state: {
      get: () = > getNestedState(store.state, path)
    }
  })

  return local
}
Copy the code

The local variable stores the context of a module.

When the module has no namespace set, the context’s dispatch method is called sotre.dispatch directly, that is, the root module’s dispatch method is called. When namespaces exist, the corresponding namespace is determined to determine which dispatch method to call

if (! options || ! Options. root) to determine whether a third argument {root: true} is passed when the dispatch method is called. If so, the corresponding dispatch method on the global root module is called

Again, the commit property in local is similar to dispatch, but I won’t go into that here

Finally, we set a layer of getters and state agents for local through the object.defineProperties method, which will be processed when they are accessed later. For example, when accessing the getters attribute, check whether there is a namespace. If there is no namespace, return store.getters. Otherwise, create a local cache of getters based on the namespace and get the corresponding getters based on the cache. Take a look at the code

// Create a local getters cache
function makeLocalGetters (store, namespace) {
  // If no getters are specified in the cache, create a new getters cache in __makeLocalGettersCache
  if(! store._makeLocalGettersCache[namespace]) {const gettersProxy = {}
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type= > {
      // If there are no getters in the store.getters that match the namespace, nothing is done
      if (type.slice(0, splitPos) ! == namespace)return

      // Get the local getters name
      const localType = type.slice(splitPos)

      // Add a layer of proxy to getters
      Object.defineProperty(gettersProxy, localType, {
        get: () = > store.getters[type],
        enumerable: true})})// Cache the proscribed getters locally
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}
Copy the code

When accessing local.getters, the store._makeLocalGettersCache is used to check whether there is a corresponding cache of getters. If not, a gettersProxy is created. Find the corresponding getters in store.getters, and then use object.defineProperty to do a layer of process for gettersProxy, i.e. when accessing local.getters. Func, Store. getters[‘first/func’] [‘first/func’] [‘first/func’] If there is a cache, it is directly obtained from the cache

The context has been created, so it’s time to register mutations, Actions, and Getters

3.2.3 Mutations of the registration module
// Register all mutations of the module
module.forEachMutation((mutation, key) = > {
  const namespacedType = namespace + key     // Example: first/second/mutations1
  registerMutation(store, namespacedType, mutation, local)
})
Copy the code

This is going through all of the mutations methods of the module, generating the namespacedType through the form of the namespace plus the mutations method name

And then I jump over to registerMutations and see how exactly are they registered

// Register mutations method
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])  Log all registered mutations through store._mutations
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
Copy the code

First, according to our incoming type, namespacedType above, we go to store._mutations to find whether there is an entry entry, and if there is, we can obtain it directly; Otherwise, we create an empty array to store the mutations method

After the entry is obtained, the current mutations method is added to the end of the entry for storage. Mutations accepts two parameters, namely state in the context and the parameter payload that we pass in

From this code, we can see that all mutations for the entire store instance are stored in store._mutations, and are stored as key-value pairs, for example:

store._mutations = {
  'mutations1': [function handler() {... }].'ModuleA/mutations2': [function handler() {... },function handler() {... }].'ModuleA/ModuleB/mutations2': [function handler() {... }}]Copy the code

The keys are composed of the namespace and the name of the mutations method, and the values are an array that holds all the mutations methods that correspond to that key

Why do we use an array? Because as I said, let’s say that the parent module ModuleA has a method called func mutations, so it looks like this in store._mutations

store._mutations = {
  'ModuleA/func': [function handler() {... }}]Copy the code

Mutations [‘ModuleA/func’], which means store._mutations[‘ModuleA/func’], is obtained when entry is obtained. But at this point, there is already a mutations method in this entry, so in order to ensure that the previous method is not replaced, it is chosen to add it to the end of the array. At this point, you should be able to guess that if you call the mutations method later, it will first obtain the corresponding array, and then iterate and execute in turn

The conclusion is that the method can be called backwards

3.2.4 Registering module Actions
// Register all actions for the module
module.forEachAction((action, key) = > {
  const type = action.root ? key : namespace + key   // Determine whether to register a global action in the namespace
  const handler = action.handler || action          // Get the function corresponding to the actions
  registerAction(store, type, handler, local)   
})
Copy the code

Iterating through all the actions methods of the module, where the handling of type and handler is mainly for compatibility between the two methods:

// The first way to write:
actions: {
  func(context, payload) {
    // Omit the business code...}}// The second way to write:
actions: {
  func: {
    root: true.handler(context, payload) {
      // Omit the business code...}}}Copy the code

When written the second way and root = true, the actions method is registered globally, that is, without any namespace prefix

Now, what is actually implemented in the registerAction method

// Register the actions method, which takes two parameters: context (which contains the dispatch, commit, getters, and state methods in the context), and the incoming parameter payload
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])   // Record all registered actions through store._actions
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // If the return value is not a Promise object, wrap a promise and take the return value as an argument to the THEN
    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

Similar to mutations, the entry entry is obtained from store._actions first, and then the current actions are packaged and added to the end of the entry. The actions method takes two arguments, the context and the parameter payload that we pass in, where the context is an object, It contains dispatches, commit, getters, state, rootGetters, and rootState. The first four are called within the context of the current module, and the last two are called globally

Finally, there is a layer of processing for the return value of the actions, because the actions are intended to handle asynchronous tasks, so we definitely want the value to be a Promise object to facilitate subsequent operations. The return value of the actions method is returned if it is a promise object. If not, it wraps a Promise object and returns the return value res as an argument to.then

Similarly, the actions method can have the same name

3.2.5 Register the getters of the module
// Register all getters for the module
module.forEachGetter((getter, key) = > {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
Copy the code

Similar to the above, I will skip to the registerGetters method

/ / register getters
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {   // If getters have been recorded, it will not be repeated
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)}return
  }
  // Record getters in store._wrappedgetters
  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

It is found that getters do not acquire an entry like mutations and actions, but directly check store._wrappedGetters[type] to see if there is a corresponding getters, if there is, it will not be repeated records; Otherwise, the wrapped getters are stored in sotre._wrappedgetters. The wrapped getters receive four parameters: state, getters, rootState and rootGetters. The first two represent state and getters in the current context, respectively, and the last two represent state and getters in the root module, respectively

So when we use Vuex, we call getters in the submodule like this:

const store = Vuex.Store({
  state: {
    a: 1.b: 2
  },
  getters: {
    addA(state) {
      return state.a + 1}},modules: {
    // Submodule A
    ModuleA: {
      state: {
        c: 3
      },
      getters: {
      	sum(state, getters, rootState, rootGetters) {
          console.log(state.c)   / / 3
          console.log(getters.addC)  / / 4
          console.log(rootState.b)  / / 2
          console.log(rootGetters.addA)  / / 2
        },
        addC(state) {
          return state.c + 1}}}}})Copy the code

Finally, we come to the conclusion that getters should not have the same name, and that the former one will not be overwritten by the latter

3.2.6 Recursively registering submodules
// Register submodules recursively
module.forEachChild((child, key) = > {
  installModule(store, rootState, path.concat(key), child, hot)
})
Copy the code

The next step is to determine if there are any nested submodules in the current module, add the name of the submodule to the end of the path, pass the appropriate argument to the installModule method, and go through all the steps in 3.2

3.3 register the vm

With the module registered above, take a look at the next line of code in Constructor:

resetStoreVM(this, state)
Copy the code

Skip to the corresponding method to see:

// Initialize the VM
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  store.getters = {}    // Set the getters object on the instance store
  
  store._makeLocalGettersCache = Object.create(null)  // Clear the local cache
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // For getters, each getter is registered with store.getters. For getters, the corresponding computed is accessed on the VM
  forEachValue(wrappedGetters, (fn, key) = > {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () = > store._vm[key],
      enumerable: true // for local getters})})const silent = Vue.config.silent
  Vue.config.silent = true
  // Use the Vue instance to store the Vuex state tree and use computed to cache the values returned by getters
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // Enable listening warnings in strict mode
  if (store.strict) {
    enableStrictMode(store)
  }

  // If an old VM exists, destroy the old VM
  if (oldVm) {
    if (hot) {
      // Dereference the old VM from state
      store._withCommit(() = > {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() = > oldVm.$destroy())
  }
}
Copy the code

The main thing to do in this method is to generate a Vue instance _VM, and then give the store._makelocalGetterscache getters and store.state to a _VM. $$state is assigned to _vm.data.$$state, and then store._makeLocalGettersCache is assigned to _VM. Computed. Getters implements computed – like functionality

Because the new _VM is generated, the old _VM is eventually destroyed by oldvm.$deStory ()

Note that it places the operation of sotre.getters in this method because later on when we access a getters, we are actually accessing something in _vm. Computed. Therefore, store.getters are handled with object.defineProperty

3.4 Visit state, mutations and actions

So far, getters can be used through store.getters. A getters, so how do you access state, mutations, actions?

3.4.1 track access to state

By searching, we define a get function in the Store class to handle store.state operations:

get state () {
  return this._vm._data.$$state
}
Copy the code

You can clearly see that when we access store.state, we are accessing store._vm.data.$$state, just as we did in _VM

3.4.2 access mutations

In fact, the visit of Mutations was touched at the very beginning, but it was only mentioned at that time, because it may not be too clear to see directly at that time

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

In Store, the store.com MIT and store.Dispatch methods are treated in a way that points to the Store. Let’s take a look at the commit method

commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]    // Check whether there is a corresponding method on _mutations
  // If the search is not found, no operation is performed
  if(! entry) {if (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)}return
  }

  // If there is a corresponding method, execute it
  this._withCommit(() = > {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(sub= > sub(mutation, this.state))

  if (
    __DEV__ &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools')}}Copy the code

First, the parameters passed in are processed by the unifyObjectStyle method. What does this method do

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (__DEV__) {
    assert(typeof type === 'string'.`expects string as the type, but found The ${typeof type}. `)}return { type, payload, options }
}
Copy the code

If you have used Vuex, you know that there are two ways to commit:

// The first mode of submission
this.$store.commit('func'.1)

// The second way to submit
this.$store.commit({
  type: 'func'.num: 1
})
Copy the code

The first parameter is determined to be an object. If it is, it is treated as an object commit style, otherwise it is returned

After processing the parameters, entry is obtained from store._mutations according to type. As analyzed above, mutations method is stored in array form, so there may be multiple methods. And then you go through the entries in the _withCommit method and you execute the mutations method in turn, because Vuex dictates that all the changes in state are through the mutations method, This attribute, store._research, is used to determine whether it is currently in the method of mutations, when the state value changes, it will first determine whether store._research is true, if not true, Means that the value of state changes without the method of mutations, so the warning ⚠️ information will be printed

I don’t know what this._subscribers is, but I don’t know what subscribers are. I don’t know what this is, and I don’t know what subscribers are

Rule 3.4.3 access actions
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  const entry = this._actions[type]  // Check whether the corresponding method exists in _actions
  // If the search is not found, no operation is performed
  if(! entry) {if (__DEV__) {
      console.error(`[vuex] unknown action type: ${type}`)}return
  }

  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .filter(sub= > sub.before)
      .forEach(sub= > sub.before(action, this.state))
  } catch (e) {
    if (__DEV__) {
      console.warn(`[vuex] error in before action subscribers: `)
      console.error(e)
    }
  }

  const result = entry.length > 1
  ? Promise.all(entry.map(handler= > handler(payload)))
  : entry[0](payload)

  return new Promise((resolve, reject) = > {
    result.then(res= > {
      try {
        this._actionSubscribers
          .filter(sub= > sub.after)
          .forEach(sub= > sub.after(action, this.state))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      resolve(res)
    }, error= > {
      try {
        this._actionSubscribers
          .filter(sub= > sub.error)
          .forEach(sub= > sub.error(action, this.state, error))
      } catch (e) {
        if (__DEV__) {
          console.warn(`[vuex] error in error action subscribers: `)
          console.error(e)
        }
      }
      reject(error)
    })
  })
}
Copy the code

The first half is similar to the COMMIT method, so I won’t go into it

In your code again, this._actionSubscribers appear, just like in COMMIT, and it’s probably the subscribers that hold the actions, so I’m not going to look at those

The variable result determines the length of the entry first. If the value is greater than 1, it indicates that there are multiple asynchronous methods, so promise. all is used for wrapping. Otherwise, simply execute entry[0]

Finally, a new promise is created and returned. The result state is determined internally. Resolve is used on success, reject is used on failure

At this point, we have implemented calls to store.state, store.getters, store.com MIT, and store.dispatch

3.5 Plug-in invocation

Moving on to the constructor code (which is the last piece of code in the constructor for the entire Store class)

// Call the passed plug-ins in turn
plugins.forEach(plugin= > plugin(this))

constuseDevtools = options.devtools ! = =undefined ? options.devtools : Vue.config.devtools
// Use vue's development plugin
if (useDevtools) {
  devtoolPlugin(this)}Copy the code

The first step is to iterate through the Plugins that were passed in when we created the Store class, calling the Plugins that were passed in (of course, we don’t pass in any of them, so Plugins are empty by default).

Then it’s time to call the devtoolPlugin method and go to the corresponding file based on the import path

// File path:./plugins/devtool.js
const target = typeof window! = ='undefined'
  ? window
  : typeof global! = ='undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if(! devtoolHook)return

  store._devtoolHook = devtoolHook

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state'.targetState= > {
    store.replaceState(targetState)
  })

  store.subscribe((mutation, state) = > {
    devtoolHook.emit('vuex:mutation', mutation, state)
  }, { prepend: true })

  store.subscribeAction((action, state) = > {
    devtoolHook.emit('vuex:action', action, state)
  }, { prepend: true})}Copy the code

Look for a long time, search for a long time, did not find which file has __VUE_DEVTOOLS_GLOBAL_HOOK__, should be dev-tools plug-in definition, in order to ensure that Vuex source code reading progress, first abandon reading dev-tools plug-in content

3.6 Other Methods

This is pretty much the whole process of generating a Store instance, but you’ll also notice that there are many methods that are not used, but are defined, and you can look at a few of them here

3.6.1 update state
// Update state with store._research = true
replaceState (state) {
  this._withCommit(() = > {
    this._vm._data.$$state = state
  })
}
Copy the code

At a glance, this provides a way to modify state directly without printing a warning message

3.6.2 Registering and Uninstalling modules
// Register the module
registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]

  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0.'cannot register the root module by using registerModule.')}this._modules.register(path, rawModule)
  installModule(this.this.state, path, this._modules.get(path), options.preserveState)
  // reset store to update getters...
  resetStoreVM(this.this.state)
}

// Uninstall the module
unregisterModule (path) {
  if (typeof path === 'string') path = [path]

  if (__DEV__) {
    assert(Array.isArray(path), `module path must be a string or an Array.`)}this._modules.unregister(path)
  this._withCommit(() = > {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)}Copy the code
3.6.3 Resetting a Store Instance
// Reset the store, that is, register the module, generate vm and so on
function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}
Copy the code

Clean up all the state and then re-execute installModule and resetStoreVM, which is usually called after the module structure has changed, such as when a module has been unloaded

4. Install the registration

Install () install () install () install () Install () Install () install () install () install () install () install ()

// Provide the install method
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.')}return
  }
  Vue = _Vue
  applyMixin(Vue)
}
Copy the code

When we call vue.use (vuex), we call this method to determine whether vuex is registered. If vuex is registered, no action is performed. If it is not registered, then the applyMixin method is called. Now go to the./mixin.js file:

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

  // The 2.x version mounts the store directly by blending it into vue. mixin globally
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // Compatible with 1.x version
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  // Insert vuex into $options
  function vuexInit () {
    // Get $options for the current component
    const options = this.$options
    // If the current component already has store on $options, then assign $options. Store to this.$store (usually used for root components)
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } 
    / / $on the options of the current component is not store, you get $store on the parent component, namely the $options. Parent. $store, and its assignment to this. $store (typically used in child components)
    else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}
Copy the code

The applyMixin method first determines the version number of Vue, and mainly makes a backward compatible version of Vue 1.x. Here I am not familiar with the version of Vue 1.x, so I will directly look at the processing method of Vue 2.x

The vuexInit method is called for each component beforeCreate lifecycle. This method is very smart. It first gets $options for the current component. $options = this.$store = this.$store = this.$store = this. If there is no store in $options, then it is not the root component, so we go to the parent component and assign the value to the current component, so the current component can also access the store instance through this.$store

And I have to say, this is a great way to do it.

5. Auxiliary functions

The store instance is generated and installed on Vue. Look at the entry file and only helper functions are left. They have mapState, mapGetters, mapMutations, mapActions, createNamespacedHelpers, so go to the corresponding file./helpers. Js and look at them

import { isObject } from './util.js'

export const mapState = normalizeNamespace((namespace, states) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(states)) {console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  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
})


export const mapMutations = normalizeNamespace((namespace, mutations) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(mutations)) {console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  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
})


export const mapGetters = normalizeNamespace((namespace, getters) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(getters)) {console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) = > {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if(namespace && ! getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if(__DEV__ && ! (valin this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})


export const mapActions = normalizeNamespace((namespace, actions) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(actions)) {console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  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
})

/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}* /
export const createNamespacedHelpers = (namespace) = > ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})


function normalizeMap (map) {
  if(! isValidMap(map)) {return[]}return Array.isArray(map)
    ? map.map(key= > ({ key, val: key }))
    : Object.keys(map).map(key= > ({ key, val: map[key] }))
}

function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}

function normalizeNamespace (fn) {
  return (namespace, map) = > {
    if (typeofnamespace ! = ='string') {
      map = namespace
      namespace = ' '
    } 
    else if (namespace.charAt(namespace.length - 1)! = ='/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}() :${namespace}`)}return module
}
Copy the code

There’s a lot of stuff in this file, but it’s clear that the main thing we’re looking at is the helper functions. Observing that each helper function calls normalizeNamespace first, let’s take a look at what this function does:

function normalizeNamespace (fn) {
  return (namespace, map) = > {
    if (typeofnamespace ! = ='string') {
      map = namespace
      namespace = ' '
    } 
    else if (namespace.charAt(namespace.length - 1)! = ='/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
Copy the code

We know from the literal meaning of the function name that this should standardize the namespace according to the different calling methods.

We first return a function that takes two arguments, namespace and map, which are the two arguments we can pass when we call the helper function.

Then judge whether the namespace is in the form of string. If not, it means that the namespace is called in an ordinary way, for example:

mapMutations(['first/second/foo', 'first/second/bar'])

mapMutations({
   foo: 'first/second/foo',
   bar: 'first/second/bar',
})
Copy the code

In this case, the first parameter namespace is directly assigned to the map variable, and namespace is set to null

If it is a string, it means that a namespace-bound function is being called, for example:

mapState('first/second'['foo'.'bar'])

mapState('first/second', {
  foo: 'foo'.bar: 'bar',})Copy the code

Once you’ve handled these two different calls, call fn with namespace and map as parameters

So let’s start with mapState

5.1 mapState
export const mapState = normalizeNamespace((namespace, states) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(states)) {console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  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

The namespace here is a string, and states is the mapping variable map that we just worked on

We first create an empty object res, which is the variable we will return after finishing processing.

Then the isValidMap method is used to determine whether the map meets the requirements, that is, whether it is an array or an object;

Then I called the normalizeMap method to handle the states variable, which is literally standardizing the variable, because it could be an array or an object, so it should be uniform. Look at the implementation of the normalizeMap method:

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

If map is not valid, return an empty array to avoid subsequent code errors.

Then check whether the map is an array. If it is an array, iterate over the map for processing:

Will [1.2.3] 变成 [{key: 1.val: 1}, {key: 2.val: 2}, {key: 3.val: 3}]
Copy the code

If map is not an array, then it must be an object, and we will also process it in the same format as above:

Will {a: 1.b: 2.c: 3[{} intokey: a, val: 1}, {key: b, val: 2}, {key: c, val: 3}]
Copy the code

Res [key] = function mappedState() {res[key] = function mappedState() {… }, let’s see what’s done in this mappedState

First, get the state and getters on the root module

// Get the root module's state and getters
let state = this.$store.state
let getters = this.$store.getters
Copy the code

Then check whether the namespace exists, that is, whether the namespace is empty. If the namespace is empty, no operation is performed. Otherwise, call the getModuleByNamespace method to obtain the module corresponding to the namespace

function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}() :${namespace}`)}return module
}
Copy the code

As you can see, store._modulesNamespacemap finally comes in handy. When the store instance is generated to register all modules, the namespaced modules are stored in this variable, which is used here

Then replace the declared variables state and getters with those in the context of the Module

if (namespace) {
  // Obtain the module corresponding to the namespace
  const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  if (!module) {
    return
  }
  // Change state and getters to state and getters in the context of the module
  state = module.context.state
  getters = module.context.getters
}
Copy the code

This context is also very clever. When registering a module, the context of the module is obtained and stored, that is:

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

I don’t know what to use when I see it before, but after I see it here, I feel really very good 👍

With the state and getters values determined, you can return the value

return typeof val === 'function'
  ? val.call(this, state, getters)
	: state[val]
Copy the code

There’s another layer here because there are two different ways to do it, for example:

mapState({
  foo: state= > state.foo,
  bar: 'bar'
})
Copy the code

Foo: (state, getters) => state.foo + getters. Bar => foo: (state, getters) => foo

5.2 mapMutations
export const mapMutations = normalizeNamespace((namespace, mutations) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(mutations)) {console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  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
})
Copy the code

Implementations of mapMutations and Mapstates are broadly similar, with the major difference being the following code:

return typeof val === 'function'
  ? val.apply(this, [commit].concat(args))
	: commit.apply(this.$store, [val].concat(args))
Copy the code

This also handles function call types and normal call types like mapState, for example:

mapMutations({
  foo: (commit, num) = > {
    commit('foo', num)
  },
  bar: 'bar'
})
Copy the code

Val. Apply (this, [commit].concat(args)); var. Apply (this, [commit].concat(args));

For a normal call type, commit is performed, val is the name of the method that needs to be called in that namespace, and then the additional parameters are accepted, Namely the commit. Apply (enclosing $store, [r]. Val concat (args))

5.3 mapGetters
export const mapGetters = normalizeNamespace((namespace, getters) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(getters)) {console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) = > {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if(namespace && ! getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if(__DEV__ && ! (valin this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
Copy the code

This also have nothing to say, to obtain a namespace namespace directly spliced val by enclosing $store. The getters (val). Here’s a quick example:

Case one

/ / the first
mapGetters(['first/foo'])
Copy the code

In this case, namespace is treated as an empty string, map is treated as [‘first/foo’], traverse the map, val = ‘first/foo’, $store.getters[‘first/foo’]

Let’s look at the second case

/ / the second
mapGetters('first'['foo'])
Copy the code

In this case, namespace is processed as first/, map is processed as [‘foo’], and val = ‘foo’, val = namespace + val is processed as first/foo, $store.getters[‘first/foo’]

5.4 mapActions
export const mapActions = normalizeNamespace((namespace, actions) = > {
  const res = {}
  if(__DEV__ && ! isValidMap(actions)) {console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  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
})
Copy the code

After a brief look, the process is almost exactly the same as mapMutations, so I won’t say much

5.5 createNamespacedHelpers
export const createNamespacedHelpers = (namespace) = > ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})
Copy the code

This method creates a set of helper functions based on the namespace passed in. The trick is to pass the first argument through the bind function first

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('first/second')

export default {
  computed: {
    ...mapState({
      a: 'a'.// equivalent to first/second/a
      b: 'b'.// equivalent to first/second/b})},methods: {
    ...mapActions([
      'foo'.// Equivalent to first/second/foo
      'bar'.// = first/second/bar])}}Copy the code

💡 Experience

First of all, I have always had the idea of reading the source code, but I did not take action because of my limited ability. Later, in a communication with the big guy, I found my own shortcomings. I did not study deeply, that is, I only stayed in the stage of using it, but I did not know why. To tell the truth, this is really uncomfortable, every time when using a library, there is a problem will only look at whether they call the way there is a problem, and then on the search engine to find the answer, for a long time so it is difficult to make progress.

So, I’m going to take a good look at the Vuex source code on my own for three reasons:

  1. VuexThe core source code is less and more friendly to people like me who are reading the source code for the first time
  2. After a thorough study of common libraries, you can quickly find the root cause of problems encountered when using them
  3. Don’t just stay on the surface of mature libraries, learn their ideas and techniques, which will help you grow

At first I didn’t know how long IT would take me to read the Vuex core source code, but I initially set myself a deadline of 15 days and expected to read at least 2 hours a day. So I fork and clone the source code of Vuex. On the first day, I simply find the location of the core code, and then have a very cursory look at the general flow of the source code. In the meantime, I went to the official Vuex documentation and went over all the core usage methods in great detail

In the following time, I will read the source code in the order I read this article

Here is a summary of a few reading source experience:

  1. To use this library must be very skilled, that is, understand the use of various methods, it is highly recommended that the official documents thoroughly (emphasis)
  2. Find the location of the core code and work your way through it, starting with the entry file
  3. Look at the source code in English comments, do not understand can use translation, these comments basically can help you understand the role of this code
  4. Encounter do not understand the place can make a note first, because it may be related to some of the code behind, and so later back to see the code before do not understand, you will understand
  5. As you read the source code, when you see a variable or a function, first look at the name, because the name basically represents what it does, and then learn to think about what a normal call would look like, so it’s easier to understand. Okay
  6. Make use of the compiler’s search capabilities. Because sometimes you see a function or variable that might be useful elsewhere, take advantage of the compiler’s search capabilities (both local and global) to help you find it.

🌱 Q&A session

Here are some of the questions the group asked me about this source reading:

Q1: What do you think of the source code? Do you watch other people’s videos or other people’s articles?

A1: Did not see other people’s video or article, at that time he thought about how to see the source code, listed a step, so groped through, think it is very interesting

Q2: Can you understand it by yourself?

A2: To tell the truth, there are some places quite difficult to understand, but combined with the source code with English comments, basically can put the general idea clear, and then do not understand the place to make a mark and temporarily omit, wait until after seeing more code, back to find that seems to understand some. The last thing to say is that the source code really is not a time to be able to understand, really is to read several times, to understand the principle

Q3: After reading the source code, can you write it out by hand?

A3: emmmm… This may be a little difficult, but I think writing some core code to achieve a simple Vuex can be done, and I think it is necessary to write the core code myself, because it is another source consolidation, and I have started to write a simple version of Vuex, Put it in the myVuex folder in the warehouse

📌 finally

If there are any mistakes in this article for Vuex source reading, you are welcome to give me suggestions, must be open-minded to listen to your corrections, think this warehouse ➡️ Vuex source reading warehouse is good, you can also point to 🌟 Star 🌟 to support me.

Finally, you can also follow my official account: Front End Impression, or add my wechat (Lpyexplore333) to communicate privately

This article I really very hard, you have the heart not to give a thumbs up 👍 ~