This article is an excerpt from the team’s open source project, InterviewMap. Project currently content contains the JS, network, browser, small procedures, performance optimization, security, framework, Git, data structures and algorithms, both basic and advanced, or source code, you can get a satisfactory answer in this map, map hope this interview can help to better prepare for the interview.

Vuex thought

Before we dive into the source code, let’s take a quick look at the idea behind Vuex.

Vuex globally maintains an object, using the singleton design pattern. In this global object, all properties are reactive, and changes to any property cause the component that uses the property to be updated. In addition, the state can only be changed through the commit mode, which implements the unidirectional data flow mode.

Vuex parsing

Vuex installation

Before looking at the next content, it is recommended that local Clone a Vuex source code contrast to see, easy to understand.

Before using Vuex, we all need to call vue.use (Vuex). During the use call, Vue calls Vuex’s install function

The install function is simple

  • Make sure that Vuex is installed only once
  • Mixed withbeforeCreateA hook function that can be used in a componentthis.$store
export function install (_Vue) {
  // Make sure that Vuex is installed only once
  if (Vue && _Vue === Vue) {
    if(process.env.NODE_ENV ! = ='production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.')}return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// applyMixin
export default function (Vue) {
  // Obtain the Vue version number
  const version = Number(Vue.version.split('. ') [0])
  BeforeCreate = beforeCreate = beforeCreate
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // ...
  }
  // The function is simple, is to let us in the component
  // 使用到 this.$store
  function vuexInit () {
    const options = this.$options
    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

Vuex initialization

this._modules

How to initialize this._modules

export class Store {
  constructor (options = {}) {
    // Introduce Vue mode, automatic installation
    if(! Vue &&typeof window! = ='undefined' && window.Vue) {
      install(window.Vue)
    }
    // Assert in the development environment
    if(process.env.NODE_ENV ! = ='production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise! = ='undefined'.`vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `store must be called with the new operator.`)}// Get the properties in options
    const { 
      plugins = [],
      strict = false
    } = options

    // Store internal state, focusing on this._modules
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    
    const store = this
    const { dispatch, commit } = this
    // bind the following two functions to this
    / / to facilitate this. $store. Dispatch
    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

Next look at the procedure for this._modules, using the following code as an example

const moduleA = {
  state: {... },mutations: {... },actions: {... },getters: {... }}const moduleB = {
  state: {... },mutations: {... },actions: {... }}const store = new Vuex.Store({
  state: {... },modules: {
    a: moduleA,
    b: moduleB
  }
})
Copy the code

For the above code, store can be thought of as root. On the first execution, it initializes a rootModule, determines if there are modules properties in root, and recursively registers the Module. For a child, it gets its parent and adds a Module to the parent.

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  register (path, rawModule, runtime = true) {
    // Development environment assertion
    if(process.env.NODE_ENV ! = ='production') {
      assertRawModule(path, rawModule)
    }
    // Initialize the Module
    const newModule = new Module(rawModule, runtime)
    // When ModuleCollection is initialized the first time
    // The first if condition is invoked, because root is currently in place
    if (path.length === 0) {
      this.root = newModule
    } else {
      // Get the parent of the current Module
      const parent = this.get(path.slice(0.- 1))
      // Add child as the first argument
      // The key of the current Module
      parent.addChild(path[path.length - 1], newModule)
    }

    // Register recursively
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // To store children
    this._children = Object.create(null)
    // Used to store raw RawModules
    this._rawModule = rawModule
    const rawState = rawModule.state

    // To store state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}
Copy the code

installModule

Next, look at the installModule implementation

// installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
  // Check whether it is a rootModule
  constisRoot = ! path.lengthRoot does not have a namespace
  // Modules: {a: moduleA
  // namespace = 'a/'
  const namespace = store._modules.getNamespace(path)

  // Cache module for namespace
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  / / set the state
  if(! isRoot && ! hot) {// The following logic is to add attributes to store.state
    // Add by module
    // state: { xxx: 1, a: {... }, b: {... }}
    const parentState = getNestedState(rootState, path.slice(0.- 1))
    const moduleName = path[path.length - 1]
    store._withCommit((a)= > {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // This method overwrites the dispatch and commit functions
  // Do you have questions about dispatch and commit in the module
  // How to find the function in the corresponding module
  // Suppose there is A mutation named add in module A
  // With the makeLocalContext function, add becomes
  // a/add, so you can find the corresponding function in module A
  const local = module.context = makeLocalContext(store, namespace, path)
  
  // The following functions are traversed in
  // Register the mutation, action and getter in the module
  // Suppose the mutation function named add is in module A
  // This will change to a/add during the registration process
  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)
  })

  // This generates a _wrappedGetters attribute
  // To cache getters for the next time
  module.forEachGetter((getter, key) = > {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
    
  // Install modules recursively
  module.forEachChild((child, key) = > {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}
Copy the code

resetStoreVM

Next, look at the implementation of resetStoreVM, which implements state responsiveness and uses _wrappedGetters as a computed attribute.

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

  // Set the getters property
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // Run through the _wrappedGetters attribute
  forEachValue(wrappedGetters, (fn, key) => {
    // Add attributes for a computed object
    computed[key] = (a)= > fn(store)
    // Override the get method
    // Store.getters
    // store._vm[xx]
    // That is, computed
    Object.defineProperty(store.getters, key, {
      get: (a)= > store._vm[key],
      enumerable: true // for local getters})})// Use Vue to save the state tree
  // Also make state reactive
  const silent = Vue.config.silent
  Vue.config.silent = true
  // When accessing store.state
  // Store._vm. _data.? state
  store._vm = new Vue({
    data: {
      ? state: state
    },
    computed
  })
  Vue.config.silent = silent

  // Ensure that the state can only be changed by committing
  if (store.strict) {
    enableStrictMode(store)
  }
}
Copy the code

Commonly used API

Commit parsing

If you need to change the state, you usually use a COMMIT. Let’s look at how a COMMIT can change the state

commit(_type, _payload, _options) {
  // Check the parameters passed in
  const { type, payload, options } = unifyObjectStyle(
    _type,
    _payload,
    _options
  )

  const mutation = { type, payload }
  // Find the corresponding mutation function
  const entry = this._mutations[type]
  // Check whether it was found
  if(! entry) {if(process.env.NODE_ENV ! = ='production') {
      console.error(`[vuex] unknown mutation type: ${type}`)}return
  }
  // The _withCommit function will do _research
  // Set this parameter to TRUE to ensure that the system is in strict mode
  // Only commit can change the state
  this._withCommit((a)= > {
    entry.forEach(function commitIterator(handler) {
      // entry.push(function wrappedMutationHandler(payload) {
      // handler.call(store, local.state, payload)
      // })
      // Handle is the wrappedMutationHandler function
      // wrappedMutationHandler is called internally
      // For the mutation function
      handler(payload)
    })
  })
  // Execute the subscription function
  this._subscribers.forEach(sub= > sub(mutation, this.state))
}
Copy the code

Dispatch parsing

If you need to change the state asynchronously, you need to do it by dispatch. Any commit function called at Dispatch is overwritten, and the mutation function in the module is found.

dispatch(_type, _payload) {
  // Check the parameters passed in
  const { type, payload } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  // Find the action function for
  const entry = this._actions[type]
  // Check whether it was found
  if(! entry) {if(process.env.NODE_ENV ! = ='production') {
      console.error(`[vuex] unknown action type: ${type}`)}return
  }
  // Trigger the subscription function
  this._actionSubscribers.forEach(sub= > sub(action, this.state))

  // When the action is registered, the function returns a value
  // When the promise is all
  // After resolve, promise.all is executed
  //
  return entry.length > 1
    ? Promise.all(entry.map(handler= > handler(payload)))
    : entry[0](payload)
}
Copy the code

Grammatical sugars

In component, if you want to use Vuex function normally, you often need to call this.$store.state. XXX, which causes a lot of inconvenience. To this end, Vuex has introduced the syntactic sugar feature, which allows us to do this in a simple way. In the following example, mapState is used as an example. Other maps have similar principles and will not be resolved one by one.

function normalizeNamespace(fn) {
  return (namespace, map) = > {
    // The function is simple
    // Generate a namespace based on parameters
    if (typeofnamespace ! = ='string') {
      map = namespace
      namespace = ' '
    } else if (namespace.charAt(namespace.length - 1)! = ='/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
// Executing mapState is executing
// Function returned by normalizeNamespace
export const mapState = normalizeNamespace((namespace, states) = > {
  const res = {}
  // normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
  // normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
  // function normalizeMap(map) {
  // return Array.isArray(map)
  / /? map.map(key => ({ key, val: key }))
  // : Object.keys(map).map(key => ({ key, val: map[key] }))
  // }
  // States can be used as an array or object type
  normalizeMap(states).forEach(({ key, val }) = > {
    res[key] = function mappedState() {
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        // Get the corresponding module
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      / / return to the State
      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 last

Above is Vue source code parsing, although the overall code of Vuex is not much, but it is a project worth reading. If you have any questions or mistakes along the way, feel free to discuss them in the comments.

If you want to learn more front-end knowledge, interview skills or some of my personal insights, you can follow my public number to learn together