Writing in the front

Since I am very interested in vue. js, and the technology stack I work in is also vue. js, I have spent some time studying the source code of vue. js in these months, and made a summary and output.

The original address of this article: github.com/answershuto… .

In the process of learning, I added the Chinese annotation github.com/answershuto… And Vuex notes github.com/answershuto… In the hope that can be helpful to other people who want to learn source code.

There may be some deviation in understanding, please mention the issue to point out that we should learn together and make progress together.

Vuex

When we use vue. js to develop complex applications, we often encounter multiple components sharing the same state, or multiple components will update the same state. When the amount of application code is small, we can communicate between components to maintain and modify data, or transfer and modify data through the event bus. However, when the application becomes increasingly large, the code becomes difficult to maintain. Starting from the parent component, the multi-layer nested data is passed through prop, which is extremely fragile due to the depth of the level. Meanwhile, the event bus will also appear complicated interaction due to the increase of components and code, and it is difficult to understand the transmission relationship among them.

So why can’t we separate the data layer from the component layer? The data layer is put into a single Store globally, and the component layer becomes thinner, dedicated to data display and operation. All data changes need to go through the global Store to form a one-way data flow, making the data changes become “predictable”.

Vuex is a library specially designed for vue.js framework and used for state management of vue.js applications. It borrows from the basic idea of Flux and Redux, extracts the shared data to the global and stores it as a singleton. Meanwhile, it uses the responsive mechanism of VUe.js to carry out efficient state management and update. Because Vuex uses the “responsive mechanism” inside Vue.js, Vuex is a framework designed specifically for vue.js and highly compatible with it (the advantage is that it is more concise and efficient, but the disadvantage is that it can only be used with Vue.js). For details on how to use the Vuex API, please refer to the Vuex website.

Let’s take a look at this data flow chart of Vuex, those of you who are familiar with the use of Vuex should already know.

Vuex implements a one-way data flow, which has a State to store data globally. All operations to modify State must be carried out by Mutation, which also provides the subscriber mode for external plug-ins to call to obtain the update of State data. Action is required for all asynchronous interfaces, which is commonly seen in calling the back-end interface to asynchronously obtain update data. Action cannot modify State directly, but Mutation is required to modify State data. Finally, depending on the State change, render to the view. Vuex is a state management library designed specifically for vue.js because it relies on Vue’s internal bidirectional data binding mechanism and requires a new Vue object to achieve “responsiveness”.

The installation

Friends who have used Vuex must know that the installation of Vuex is very simple, just need to provide a store, and then execute the following two sentences of code to complete the introduction of Vuex.

Vue.use(Vuex);

/* Put store in option when Vue is created */
new Vue({
    el: '#app',
    store
});Copy the code

How does Vuex inject a Store into a Vue instance?

Use method is used to install the plug-in to vue. js. Internally, the plug-in is installed by calling the install method of the plug-in (when the plug-in is an object).

Let’s look at the install implementation of Vuex.

/* Expose the external plug-in install method for vue.use to call the install plug-in */
export function install (_Vue) {
  if (Vue) {
    /* Avoid repeated installation (vue. use also internally detects if the same plug-in is installed again) */
    if(process.env.NODE_ENV ! = ='production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.')}return
  }
  /* Save Vue and check for repeated installation */
  Vue = _Vue
  /* Blur vuexInit into Vue's beforeCreate(Vue2.0) or _init method (Vue1.0)*/
  applyMixin(Vue)
}Copy the code

This install code does two things. One is to prevent Vuex from being installed again, and the other is to execute the applyMixin in order to execute the vuexInit method to initialize Vuex. Vuex has different processing for Vue1.0 and 2.0 respectively. If it is Vue1.0, Vuex will put the vuexInit method into the _init method of Vue, while for Vue2.0, it will confuse vuexInit into the beforeCreacte hook of Vue. Take a look at the vuexInit code.

 /* Init hooks for Vuex are stored in the hook list for each instance of Vue */
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      /* If there is a store, the Root node exists. If there is a store, the Root node exists. If there is a store, the Root node exists
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /* The child component obtains the $store from the parent component directly, thus ensuring that all components share the same global store*/
      this.$store = options.parent.$store
    }
  }Copy the code

VuexInit will try to get the store from options. If the current component is the Root component (Root node), there will be a store in Options. Just get the value and assign it to $store directly. If the current component is not the root component, the parent component’s $Store reference is obtained through the parent in Options. This way, all components get the same Store instance with the same memory location, so we can happily access the global Store instance from this.$Store in each component.

So, what is a Store instance?

Store

We pass in the root component to the store, which is the store instance, constructed using the Vuex provided to the store method.

export default new Vuex.Store({
    strict: true.modules: {
        moduleA,
        moduleB
    }
});Copy the code

Let’s look at the Store implementation. The first is the constructor.

constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    /* In the browser environment, if the plug-in is not installed (! Vue is not installed), it will be installed automatically. It allows users to avoid automatic installation in some cases. * /
    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.`)}const {
      /* An array containing the plugin methods applied to the Store. These plug-ins directly accept Store as the only parameter and can either listen for mutation (for external data persistence, recording, or debugging) or submit mutation (for internal data, such as websocket or some observers) */
      plugins = [],
      /* Make the Vuex Store enter strict mode, in which any change to Vuex state outside of the mutation handler will throw an error. * /
      strict = false
    } = options

    /* Take state from option, execute if state is function, and end up with an object */
    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    /* Is used to determine whether the state */ is modified with mutation in strict mode
    this._committing = false
    /* Save action */
    this._actions = Object.create(null)
    /* Store mutation */
    this._mutations = Object.create(null)
    /* Store the getter */
    this._wrappedGetters = Object.create(null)
    /* Module collector */
    this._modules = new ModuleCollection(options)
    /* Store module */ according to namespace
    this._modulesNamespaceMap = Object.create(null)
    /* Deposit subscriber */
    this._subscribers = []
    /* Vue instance for Watch */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /* Bind the this of dispatch and commit calls to the Store object itself, otherwise this at this.dispatch will point to the component's VM */ inside the component
    const store = this
    const { dispatch, commit } = this
    /* Bind this (the Store instance itself) to dispatch and commit */
    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
    /* Strict mode (puts Vuex Store in strict mode, where any change to Vuex state outside of the mutation handler will throw an error)*/
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    /* Initializes the root module, this also recursively registers all child modles, collecting all module getters into _wrappedGetters, this._modules.root represents the module object that is held only by the root module */
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* New Vue objects are registered for state and computed */ using the response-based internal Vue
    resetStoreVM(this, state)

    // apply plugins
    /* Call the plugin */
    plugins.forEach(plugin= > plugin(this))

    /* Devtool plugin */
    if (Vue.config.devtools) {
      devtoolPlugin(this)}}Copy the code

In addition to initializing some internal variables, the Store constructor class executes installModule (initializing the Module) and resetStoreVM (making the Store “responsive” through the VM).

installModule

InstallModule is used to add namespace namespaces (if any) to modules, register mutations, actions, and getters, and recursively install all submodules.

/* Initialize module*/
function installModule (store, rootState, path, module, hot) {
  /* Is the root module */
  constisRoot = ! path.length/* Obtain the namespace of module */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* If there is a namespace, register it in _modulesNamespaceMap */
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if(! isRoot && ! hot) {/* Get the parent state */
    const parentState = getNestedState(rootState, path.slice(0.- 1))
    /* module的name */
    const moduleName = path[path.length - 1]
    store.`_withCommit`((a)= > {
      /* Set the submodule to a responsive */
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

  /* Pass the registration mutation */
  module.forEachMutation((mutation, key) = > {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  /* Iterate over the registration action */
  module.forEachAction((action, key) = > {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  /* Iterates over registered getters */
  module.forEachGetter((getter, key) = > {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

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

resetStoreVM

Before we talk about resetStoreVM, let’s look at a small demo.

let globalData = {
    d: 'hello world'
};
new Vue({
    data () {
        return {
            ? state: {
                globalData
            }
        }
    }
});

/* modify */
setTimeout((a)= > {
    globalData.d = 'hi~';
}, 1000);

Vue.prototype.globalData = globalData;

/* In any template */
<div>{{globalData.d}}</div>Copy the code

This code has a globalData that is passed into the data of a Vue object and is then displayed in any Vue template. Since globalData is already on Vue’s prototype, it is accessed directly through this.prototype. That is {{prototype.d}} in the template. At this point, setTimeout modifies globalData.d after 1s, and we find that globalData.d in the template has changed. In fact, the above part is that Vuex relies on Vue core to achieve “responsive” data.

For those who are not familiar with the reactive principles of vue. js, I have another article, Reactive Principles, to understand how vue. js is used for two-way data binding.

Let’s look at the code.

/* New Vue objects are registered for state and computed */ using the response-based internal Vue
function resetStoreVM (store, state, hot) {
  /* Store previous VM objects */
  const oldVm = store._vm 

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* Set the get method for each getter method using object.defineProperty. For example, when this.$store.getters. Test is obtained, store._vm. Test is obtained
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = (a)= > fn(store)
    Object.defineProperty(store.getters, key, {
      get: (a)= > 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 is temporarily set to true so that no warning is reported during the new instance of Vue */
  Vue.config.silent = true
  /* A new Vue object is used to register state and computed*/ in Vue
  store._vm = new Vue({
    data: {
      ? state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* Enable strict mode to ensure that store can be modified only by mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* Remove the reference to the old VM state and destroy the old Vue object */
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit((a)= >{ oldVm._data.? state =null
      })
    }
    Vue.nextTick((a)= > oldVm.$destroy())
  }
}Copy the code

ResetStoreVM first runs through wrappedGetters, binding the get method to each getter using the object.defineProperty method, This way we can access this.$store.getter.test in the component equivalent to store._vm.test.

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

After that, Vuex adopted a new Vue object to realize the “responsive” of data, and used the data bidirectional binding function provided by vue.js to realize the synchronous update of data and view of store.

store._vm = new Vue({
  data: {
    ? state: state
  },
  computed
})Copy the code

At this point we access store._vm.test, which accesses the properties in the Vue instance.

After these two steps, we can access the test property in the VM through this.$store.getter.test.

Strict mode

The Option of the Store construct class of Vuex has a strict parameter that controls the execution of the strict mode by Vuex. In strict mode, all operations to modify state must be implemented by mutation, otherwise an error will be thrown.

/* Enable strict mode */
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.? state }, () => {if(process.env.NODE_ENV ! = ='production') {
      /* Check the value of _research in store, true means */ was not changed by the mutation method
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)}}, {deep: true.sync: true})}Copy the code

First, in strict mode, Vuex uses the VM’s $watch method to see? State, the state of the Store, enters the callback when it is modified. What we find is that there’s only one sentence in the callback that checks for store._research with an assert assertion, which is thrown when store._research is false.

We found that in the COMMIT method of Store, the statement that executes mutation looks like this.

this._withCommit((a)= > {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})Copy the code

Let’s look at the implementation of _withCommit.

_withCommit (fn) {
  /* The withCommit call changes the value of state and sets store research to true, which is checked by an internal assertion, and in strict mode only mutation is allowed to change the value of store, not the value of store directly */
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}Copy the code

We find that when we change the state data with commit (mutation), we set research to true before calling the mutation method, and then change the data in state with the mutation function. This case doesn’t throw an exception when it triggers the callback assertion, Research, in $watch (this case, research is true). When we modify the state data directly, the $watch callback is invoked to execute the assertion, which suggests false, and an exception is thrown. This is the implementation of the strict mode of Vuex.

Next, let’s look at some of the apis provided by the Store.

Commit (mutation)

/* Call the commit method of mutation */
commit (_type, _payload, _options) {
  // check object-style commit
  /* Check parameter */
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  /* Method of retrieving the mutation corresponding to type */
  const entry = this._mutations[type]
  if(! entry) {if(process.env.NODE_ENV ! = ='production') {
      console.error(`[vuex] unknown mutation type: ${type}`)}return
  }
  /* Execute all methods */ in mutation
  this._withCommit((a)= > {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  /* Notify all 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')}}Copy the code

The COMMIT method will find and call the mutation methods corresponding to all types in _mutations based on type, so when there is no namespace, the COMMIT method will trigger the mutation methods on all modules. All subscribers in _subscribers are executed after all mutations have been performed. So let’s see what _subscribers is.

The Store provides an external subscribe method to register a subscription function, which is pushed into _subscribers of the Store instance and returns a method to unsubscribe that subscriber from _subscribers.

/* Register a subscription function and return the unsubscribed function */
subscribe (fn) {
  const subs = this._subscribers
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return (a)= > {
    const i = subs.indexOf(fn)
    if (i > - 1) {
      subs.splice(i, 1)}}}Copy the code

These subscribers in _subscribers are called after the commit, and this subscriber mode provides an external possibility to monitor state changes. When state is changed by mutation, these changes can be effectively recovered.

Dispatch (action)

Take a look at the Dispatch implementation.

/* Call the action's dispatch method
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  /* Remove the ation */ corresponding to type from the actions
  const entry = this._actions[type]
  if(! entry) {if(process.env.NODE_ENV ! = ='production') {
      console.error(`[vuex] unknown action type: ${type}`)}return
  }

  /* if the Promise is an array, wrap it to form a new Promise. If only one Promise is available, return the 0th */
  return entry.length > 1
    ? Promise.all(entry.map(handler= > handler(payload)))
    : entry[0](payload)
}Copy the code

And registerAction.

/* Iterate over the registration action */
function registerAction (store, type, handler, local) {
  /* Retrieve the action */ corresponding to the type
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    /* Determine whether Promise */
    if(! isPromise(res)) {/* If it is not a Promise object, convert it to a Promise object */
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      /* Trigger vuex error to devtool */ when devtool is present
      return res.catch(err= > {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}Copy the code

Because registerAction wraps the actions that are pushed into _actions (wrappedActionHandler), we get the methods state, commit, and so on in the first argument to dispatch. The res is then checked to see if it is a Promise, and if it is not, it is encapsulated and converted into a Promise object. If there is only one, it is returned directly. Otherwise, promise. all is used and returned.

watch

/* Observe a getter method */
watch (getter, cb, options) {
  if(process.env.NODE_ENV ! = ='production') {
    assert(typeof getter === 'function'.`store.watch only accepts a function.`)}return this._watcherVM.$watch((a)= > getter(this.state, this.getters), cb, options)
}Copy the code

Those familiar with Vue should be familiar with the method of Watch. In a clever twist of design, _watcherVM is an instance of Vue, so watch can directly take advantage of the watch feature inside Vue, which provides a way to observe changes in the getter data.

registerModule

/* Register a dynamic module. When services are loaded asynchronously, you can register the dynamic module through this interface */
registerModule (path, rawModule) {
  /* Array */
  if (typeof path === 'string') path = [path]

  if(process.env.NODE_ENV ! = ='production') {
    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.')}/ * * / registration
  this._modules.register(path, rawModule)
  /* Initialize module*/
  installModule(this.this.state, path, this._modules.get(path))
  // reset store to update getters...
  /* New Vue objects are registered for state and computed */ using the response-based internal Vue
  resetStoreVM(this.this.state)
}Copy the code

RegisterModule is used to register a dynamic module, that is, to register the module after the store has been created. The internal implementation is actually only installModule and resetStoreVM, which have already been covered and won’t be repeated here.

unregisterModule

 /* Log off a dynamic module */
unregisterModule (path) {
  /* Array */
  if (typeof path === 'string') path = [path]

  if(process.env.NODE_ENV ! = ='production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)}* / / * revoked
  this._modules.unregister(path)
  this._withCommit((a)= > {
    /* Get the parent state */
    const parentState = getNestedState(this.state, path.slice(0.- 1))
    /* Delete */ from the parent
    Vue.delete(parentState, path[path.length - 1])})/* Rework store */
  resetStore(this)}Copy the code

Similarly, the method corresponding to registerModule unregisterModule dynamically logs out the module. This is done by first removing the module from state and then remaking the store with resetStore.

resetStore

/* Rework store */
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

After initializing the _actions in the store, re-execute installModule and resetStoreVM to initialize the Module and make it “responsive” with the Vue feature, as in the constructor.

The plug-in

Vue provides a very useful plugin called vue.js Devtools

/* Get the devTool plugin from the window object's __VUE_DEVTOOLS_GLOBAL_HOOK__
const devtoolHook =
  typeof window! = ='undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

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

  /* The devToll plug-in instance is stored in _devtoolHook */
  store._devtoolHook = devtoolHook

  /* Start the vuex initialization event and pass the reference to the store to delTool so that the delTool gets an instance of the store */
  devtoolHook.emit('vuex:init', store)

  /* Monitor travel-to-state events */
  devtoolHook.on('vuex:travel-to-state', targetState => {
    /* Rework state */
    store.replaceState(targetState)
  })

  /* Subscription store changes */
  store.subscribe((mutation, state) = > {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}Copy the code

If the plug-in is already installed, a VUE_DEVTOOLS_GLOBAL_HOOK will be exposed on the Windows object. DevtoolHook is used to notify the plugin by triggering the ‘vuex:init’ event upon initialization, and then to reset state by listening for the ‘vuex:travel-to-state’ event via the on method. Finally, a subscriber is added via the subscribe method of the Store, which is notified when the commit method is triggered to modify the mutation data, triggering the “VUex :mutation” event.

The last

Vuex is a very good library with a small amount of code and a clear structure, which is very suitable for studying its internal implementation. I have also benefited from a series of recent source code readings, and I am writing this article in the hope that it will help more students who want to learn how to explore the internal implementation of Vuex.

about

Author: dye mo

Email: [email protected] or [email protected]

Github: github.com/answershuto

Blog: answershuto. Making. IO /

Zhihu homepage: www.zhihu.com/people/cao-…

Zhihu column: zhuanlan.zhihu.com/ranmo

The Denver nuggets: juejin. Im/user / 289926…

OsChina:my.oschina.net/u/3161824/b…

Reproduced please indicate the source, thank you.

Welcome to follow my public account