Decrypt Vuex: Start with source code

Many times when we develop a Vue project, we use EventBus encapsulated by a Vue instance to handle the transfer of events so as to achieve the sharing of state between components. However, as the complexity of the business increases, the state shared between components becomes difficult to trace and maintain. Therefore, we need to save these shared states through a global singleton object and update the state update component through a specified method.

Review of basic knowledge

Since vuex is said to be a way to solve data communication between components, let's first review several methods of communication between components:

props transfer value

This method allows us to pass the value of the parent component directly to the child component and call it in the child component. Obviously, props is a one-way data binding, and the subcomponents cannot modify the value of props. In vue1.x, bidirectional binding can be achieved by. async, but this bidirectional binding is difficult to locate the source of data errors. In vue2.3.0 version, async is added back.

// Parent component
<Child name="hahaha" />

// Subcomponents
<div>{{name}}</div>
// ...
props: ['name']
// ...

$on $emit

If the child component passes data to the parent component, we can register events in the child component through $emit and $on, listen for events in the parent component and make callbacks.

// Parent component
<Child @getName="getNameCb" />
// ...
getNameCb(name) {
  console.log('name');
}

// Subcomponents
someFunc() {
  this.$emit('getName', 'hahahah');
}

EventBus

The first two methods can easily solve the communication problem between parent and child components, but it's hard to deal with the communication between sibling components or grandchildren components. You need to transfer props layer by layer and $emit layer by layer. Then you can actually use EventBus, which is actually an instance of Vue. We publish and subscribe to events through $emit and $on of the Vue instance. But the problem is obvious, too much use of EventBus can also cause data source traceability problems, and not timely through the $off cancellation event, there will also be many wonderful things.

import EventBus from '...';

// A component
// ...
mounted() {
  EventBus.$on('someevent', (data) => {
    // ...
  })
}
// ...

// Some other component
// ...
someFunc() {
  EventBus.$emit('someevent', 'hahahah');
}
// ...

Vuex

Next we will talk about Vuex. These problems can be solved by Vuex. Vuex is also a member of the Vue family bucket maintained by the official team of Vue. As the son of Vue, Vuex is undoubtedly very suitable for Vue project. However, Vuex is not perfect, there is no doubt that adding a layer of Store to the application will increase the cost of learning and maintenance, and to make it clear that there are few components in a small project, Vuex will only increase the amount of your code, use it as appropriate. Now let's go to the body of our Vuex source learning.

Analysis Principle

  • State: The state here is a single state tree.
  • mutations: Synchronization events are triggered here, and state can be modified directly.
  • actions: commit mutation s and perform asynchronous operations;
  • getters: This graph omits getter and can get state through getter. It will also be transformed into the computed attribute of vue instance (_vm) inside vuex to achieve response.

Review the design principles of Vuex. We store the shared state between components in the state of Vuex, and the component renders according to the value of the state. When we need to update state, we call the dispatch method provided by Vuex to trigger action, commit method to submit a mutation in action, and modify state directly through mutation. The component listens to the update of state and finally updates the component. It should be noted that mutaion can't perform asynchronous operations, which need to be done in action; mutation is the only one that directly modifies state. (Specific methods of use I will not go verbose, official documents written in detail, and the Chinese version, why not read...)

In my opinion, the function of Vuex is to solve the sharing of state between components, make the project more convenient for maintenance, and also implement the concept of one-way data flow. But in fact, Vuex is also like a front-end "database" in terms of function. When we use Vuex, it is very similar to the back-end classmates'addition, deletion and modification of the library.

In Vue projects, we can also use Redux to handle shared state, or even simply encapsulate a tool to handle state. After all, introducing Vuex is also a cost for developing students. But in the final analysis, it's all the idea of one-way data flow, one-way data flow, one-way data flow, one-way data flow, one-way data flow, one-way data flow, one-way data flow, one-way data flow and one-way data flow.

As an aside, I don't want to share the front and back state with Vuex when I study Vue ssr. So I encapsulate the Vue instance based on EventBus, which also realizes the function of Vuex. Interested students can see. Poke here.

Analysis of Source Code

First, we print out the Vue instance that has mounted the Vuex instance to see what has been added after mounting.

Unlike vue-router, which adds a lot of custom attributes to the instance of Vue, some are just a $store attribute, pointing to the initialized Vuex instance.

Project structure

To get the source code of a project, we need to browse its directory structure first.

src is our source code part:

  • Helers. JS are some basic API s of Vuex, such as mapState and mapActions.
  • index.js and index.esm.js are our entry files. The difference is that index.esm.js is written by EcmaScript Module.
  • mixin.js is a function encapsulated by mixin.
  • Module is the source code of module related logic in Vuex.
  • In plugins, we encapsulate the logic related to devtool and log.
  • store.js is the main logic, which encapsulates a Store class.
  • util.js is the encapsulation of some tool functions.

Application Entry

Usually when you build a program that contains Vuex, you write as follows:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex({
  state: {...},
  mutations: {...},
  actions: {...},
});

new Vue({
  store,
  template,
}).$mount('#app')  

Small partners who have used Redux can find that Vuex is an object-oriented configuration, which is different from the "partial function initialization" of redux. It is easier for developers to understand. And Vuex is installed on the Vue instance as a plug-in.

Installation plug-in

In store.js, an export function install that conforms to the Vue plug-in mechanism is defined, and a mixin of beforeCreate is encapsulated.

Source location: / src/store.js/src/mixin.js

// store.js
// ...
// Bind a Vue instance;
// Some static methods provided by Vue can be used without packaging Vue into the project.
let Vue
// ...
// Vue plug-in mechanism
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
  // Package mixin mount $store
  applyMixin(Vue)
}
// mixin.js
export default function (Vue) {
  // Get version number
  const version = Number(Vue.version.split('.')[0])
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // Compatible with low version Vue
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }
  // Encapsulation mixin;  
  // Bind the $store instance;
  // The $store of the subcomponent always points to the store instance mounted by the root component.
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      // Store may be a factory function. To avoid state cross-contamination in vue ssr, store is usually encapsulated with factory function.
      this.$store = typeof options.store === 'function'
        ? options.store() 
        : options.store
    } else if (options.parent && options.parent.$store) {
      // The child component refers to the $store property from its parent component, nested settings
      this.$store = options.parent.$store
    }
  }
}

The simple thing to do here is to bind a $store attribute to the Vue instance in the beforeCreate hook to point to the Store instance we defined. In addition, you can see that Vuex also uses a very common method of exporting a Vue instance, so that you can use some of the methods provided by Vue without packaging Vue into a project.

Instantiate Store

To instantiate the Store class, let's first look at the constructor of the Store class:

Source location: / src/store.js

constructor (options = {}) {
    // If there is a Vue instance on Windows, install the plug-in directly.
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    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.`)
    }
    // The configuration items passed in when instantiating the store;
    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    // Collect commit
    this._committing = false
    // Collect action
    this._actions = Object.create(null)
    // action Subscriber
    this._actionSubscribers = []
    // Collecting mutation s
    this._mutations = Object.create(null)
    // Collecting Getters
    this._wrappedGetters = Object.create(null)
    // Collection module
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    // Vue instances to handle state changes
    this._watcherVM = new Vue()
    // Point this of dispatch and commit calls to the Store instance;
    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)
    }
    // strict mode
    this.strict = strict
    // Getting state
    const state = this._modules.root.state
    // The main function is to generate a map of namespace, mount action, mutation, getter;
    installModule(this, state, [], this._modules.root)
    // By resetting the store in vm, new Vue objects are registered with state and computed using the responsiveness inside Vue.
    resetStoreVM(this, state)
    // Using plug-ins
    plugins.forEach(plugin => plugin(this))
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }

You can see that in the whole constructor, the main thing is to declare some basic variables, and then the most important thing is to execute the intsllModule function to register Module and resetStoreVM to make Store "responsive".
As for ModuleCollection related code, let's not go into it for the moment, knowing that he is a Module collector and provides some methods.

Next, look at the two main methods, first installModule, in which you go back to generate namespaces, and then mount mutation, action, getter:

Source location: / src/store.js

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)
  // Map Generating name and Modele
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // Register the response for module;
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  const local = module.context = makeLocalContext(store, namespace, path)
  // Mount mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  // Mount action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  // Mount getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  // Recursive Installation Module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

// ...
// Registration mutation
function registerMutation (store, type, handler, local) {
  // Find the mutation array for the corresponding type in _mutations
  // If it is created for the first time, it is initialized to an empty array
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // push A wrapped function with payload parameters
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
// Registration action
function registerAction (store, type, handler, local) {
  // Find the corresponding action according to the type. 
  const entry = store._actions[type] || (store._actions[type] = [])
  // push A wrapped function with payload parameters
  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 res is not a promise object, convert it into a promise object
    // This is because the Promise.all() method in the store.dispatch method.
    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
    }
  })
}
// Register getter
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 all defined getter s in _wrapped Getters;
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

In the module of Vuex, we can split many modules, and each module can be mounted on the parent module as a brand new module. So we need a path variable to distinguish the hierarchical relationship. We can get the state under each module according to this path. mutation, action, etc.

Next comes the resetStoreVM method, which binds a _vm attribute to the store pointing to a new Vue instance and passes in state and computed, which is the getter we set in the store.

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

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // Set get for each getter;
  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
  // Bind Vue instances for store s and register state and computed
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }
  // Remove Binding Old vm
  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())
  }
}

dispatch and commit

There are two important operations in Vuex, one is dispatch and the other is commit. We trigger an action by dispatch, and then we update the state by commit in action. Now let's look at the source code of these two departments.

Source location: / src/store.js

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

    const mutation = { type, payload }
    // Find mutation method corresponding to type.
    const entry = this._mutations[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // Perform mutation;
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // Notify Subscribers
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }
  
  dispatch (_type, _payload) {
    // check object-style dispatch
    // Test value;
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    // Get the action corresponding to the type;
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // Notify action subscribers;
    this._actionSubscribers.forEach(sub => sub(action, this.state))
    // Return action
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }

Static methods provided

Vuex provides us with some static methods to manipulate our state, mutation, action and getter by calling Store instances bound to Vue instances.

Source location: / src/helpers.js

//Return an object
//The property name of the object corresponds to the property name or array element of the incoming states
//The return value of executing this function varies according to val
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
})
// Return an object
// Executing this function triggers the specified mutation 
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // thie namespace has been mutate by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && !(val in 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 = {}
  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
})
// Accept an object or array, and eventually convert it into an array. The array element is an object that contains two attributes, key and value.
function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
function normalizeNamespace (fn) {  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

epilogue

The author did not paste out all the source code for line-by-line analysis, but simply analyzed the source code of the core logic. Generally speaking, Vuex source code is not much, written very concise and easy to understand, I hope everyone can take time to see the source code for learning.

Tags: Javascript Vue Attribute Database ECMAScript

Posted on Wed, 04 Sep 2019 00:12:10 -0700 by shesma