The Vuex version interpreted in this article is 2.3.1
Vuex code structure
The code for Vuex is not much, but it is small and complete. Let’s take a look at the implementation details.
Source code analysis
Entrance to the file
Entry file SRC /index.js:
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions
}Copy the code
This is the EXPOSED API of Vuex, the core of which is Store, followed by Install, which is a necessary method for a Vue plug-in. Both Store and install are in the store.js file. MapState, mapMutations, mapGetters, and mapActions are four auxiliary functions, which are used to map the relevant attributes in the store to the components.
The install method
All Vuejs plug-ins should have an install method. Let’s take a look at the usual poses we use with Vuex:
import Vue from 'vue'
import Vuex from 'vuex'. Vue.use(Vuex)Copy the code
Install method source:
export function install (_Vue) {
if (Vue) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
applyMixin(Vue)
}
// auto install in dist mode
if (typeof window! = ='undefined' && window.Vue) {
install(window.Vue)
}Copy the code
The _Vue input to the method is the Vue constructor passed in when use is used. The install method is simple. It first determines if Vue already has a value and throws an error. Here, Vue is an internal variable declared at the beginning of the code.
let Vue // bind on installCopy the code
This is to ensure that the install method is executed only once. The applyMixin method is called at the end of the install method. This method is defined in SRC /mixin.js:
export default function (Vue) {
const version = Number(Vue.version.split('. ') [0])
if (version >= 2) {
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > - 1
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/** * Vuex init hook, injected into each instances init hooks list. */
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}Copy the code
When the vUE version is >=2, a global mixin is added to the vUE, either in the init or beforeCreate phase. Global mixins added to Vue affect every component. Mixins can be mixed in different ways, with the hook function of the same name being mixed into an array and therefore being called. Also, hooks for the mixed object will precede hooks for the component itself.
Take a look at what the mixin method vueInit does: This.$options is used to get the instance’s initialization options. When a store is passed in, mount the store to the instance’s $store. In this way, we can access Vuex’s various data and states through this.$store.xxx in Vue’s component.
Store constructor
The most code in Vuex is store.js, whose constructor is the main flow of Vuex.
constructor (options = {}) {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise! = ='undefined'.`vuex requires a Promise polyfill in this browser.`)
const {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}
// store internal state
this._committing = false
this._actions = Object.create(null)
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()
// bind commit and dispatch to self
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
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin= > plugin(this))}Copy the code
Again, let’s look at the usual posture for using Store so we know the method’s entry:
export default new Vuex.Store({
state,
mutations
actions,
getters,
modules: {... }, plugins,strict: false
})Copy the code
The store constructor starts with two judgments.
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise! = ='undefined'.`vuex requires a Promise polyfill in this browser.`)Copy the code
Here assert is a method in util.js.
export function assert (condition, msg) {
if(! condition)throw new Error(`[vuex] ${msg}`)}Copy the code
Verify that Vue exists to ensure that the store has already been installed. In addition, Vuex relies on Promise, which is also judged here. The assert function is simple, but it’s a good way to learn about programming. Here we go:
const {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}Copy the code
The plugins, strict, and state are obtained by deconstructing and setting default values to get the values passed in. The state passed in can also be a method, and the return value of the method is the state.
Then we define some internal variables:
// store internal state
this._committing = false
this._actions = Object.create(null)
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()Copy the code
This._research indicates the commit state, which ensures that changes to state in Vuex can only be made in the mutation callback function, and can’t be made externally. This._actions is used to store all the actions defined by the user. This._mutations is used to store all of the user-defined mutatins. This._wrappedGetters is used to hold all getters defined by the user. This._modules stores all modules defined by the user. This._modulesnamespacemap stores the mapping between a module and its namespace. This._subscribers is used to store all subscribers with mutation changes. This._watchervm is an instance of a Vue object that uses the Vue instance method $watch to observe changes. These arguments will be used later, and we’ll expand them out.
Keep reading:
// bind commit and dispatch to self
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
Bind the Store class’s dispatch and commit methods to the current Store instance, just like the comments to the code. The implementation of Dispatch and COMMIT will be examined later. Strict indicates whether to enable the strict mode. In the strict mode, all state changes are observed. It is recommended to enable the strict mode in the development environment and to disable the strict mode in the online environment; otherwise, certain performance costs will be incurred.
At the end of the constructor:
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin= > plugin(this))Copy the code
The initialization core of Vuex
installModule
Using a single state tree results in all the state of the application being lumped into one large object. However, as the app gets big, the Store object becomes bloated.
To solve this problem, Vuex allows us to split the store into modules. Each module has its own state, mutation, action, getters, and even nested submodules — similarly split from top to bottom.
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)Copy the code
Before entering the installModule method, it is important to look at what the method’s input argument, this._modules.root, is.
this._modules = new ModuleCollection(options)Copy the code
SRC /module/module-collection.js and SRC /module/module.js are used here
module-collection.js:
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.root = new Module(rawRootModule, false)
// register all nested modules
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
this.register([key], rawModule, false)})}... }Copy the code
The module-collection constructor defines the root property of the instance as a module instance. Then, iterating through options’ modules to register.
Take a look at the Module constructor:
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
...
}Copy the code
Here the rawModule is passed one layer at a time, that is, new Store options. The _children of the module instance is currently null, and then the _rawModule and state of the instance are set.
Return to the register method of the module-collection constructor and the related methods it uses:
register (path, rawModule, runtime = true) {
const parent = this.get(path.slice(0.- 1))
const newModule = new Module(rawModule, runtime)
parent.addChild(path[path.length - 1], newModule)
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
get (path) {
return path.reduce((module, key) = > {
return module.getChild(key)
}, this.root)
}
addChild (key, module) {
this._children[key] = module
}Copy the code
The input parameter path of the get method is an array, such as [‘subModule’, ‘subsubModule’]. The reduce method is used here, and the values are set layer by layer. This.get (path.slice(0, -1)) obtains the parent module of the current module. We then call the addChild method of the Module class to add the changed Module to the _children object of the parent Module.
Then, if modules are passed to rawModule, it recurses once to register.
Take a look at the resulting _modules data structure:
I’m going to go a long way to explain the input parameters to the installModule function, and then go back to the installModule method.
constisRoot = ! path.lengthconst namespace = store._modules.getNamespace(path)Copy the code
The root module is determined by the length of the path.
Take a look at the getNamespace method:
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) = > {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : ' ')},' ')}Copy the code
The reduce method is used to add the module names. The module.namespaced argument is used to define the module, for example:
export default {
state,
getters,
actions,
mutations,
namespaced: true
}Copy the code
So if you define a store like this, the namespace of your selectLabelRule will be ‘selectLabelRule/’.
export default new Vuex.Store({
state,
actions,
getters,
mutations,
modules: {
selectLabelRule
},
strict: debug
})Copy the code
Next, look at the installModule method:
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}Copy the code
If namespaced is passed true, put the module on an internal variable _modulesNamespaceMap object based on its namespace.
then
// set state
if(! isRoot && ! hot) {const parentState = getNestedState(rootState, path.slice(0.- 1))
const moduleName = path[path.length - 1]
store._withCommit((a)= > {
Vue.set(parentState, moduleName, module.state)
})
}Copy the code
GetNestedState is similar to the previous getNamespace. It also uses Reduce to obtain the state of the current parent Module, and finally calls wue. set to add the state to the state of the parent module.
Look at the _withCommit method here:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}Copy the code
“This._research” is declared in the Store constructor with an initial value of false. Here, since we’re modifying state, all changes to state in Vuex are wrapped in _withCommit, ensuring that this. _research is true throughout the synchronization of the state change. This way when we look at the state change, if this._research is not true, we can check that this state change is problematic.
In Vuex source code example/shopping-cart for example, open store/index.js, and there is a code like this:
export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})Copy the code
We have two sub-modules, Cart and Products. We open store/modules/cart.js and look at the state definition in the Cart module.
const state = {
added: [].checkoutStatus: null
}Copy the code
Run the project, open the browser, and use the debugging tool of Vue to see the state in Vuex, as shown below:
Look at the end of the installModule method:
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) = > {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) = > {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
module.forEachGetter((getter, key) = > {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) = > {
installModule(store, rootState, path.concat(key), child, hot)
})Copy the code
Local is the input parameter for the next few methods, so we need to look at the makeLocalContext method again:
/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ' '
const local = {
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) = > {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if(! options || ! options.root) { type = namespace + typeif(! store._actions[type]) {console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return}}return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) = > {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if(! options || ! options.root) { type = namespace + typeif(! store._mutations[type]) {console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? (a)= > store.getters
: (a)= > makeLocalGetters(store, namespace)
},
state: {
get: (a)= > getNestedState(store.state, path)
}
})
return local
}Copy the code
As the annotation for the method says, the method is used to get local dispatches, commits, getters, and states, and if there is no namespace, the root store dispatch, commit, and so on
Take local.dispath as an example. If no namespace is set to “, use this.dispatch. If there is a namespace, add namespace to type and dispath.
With the local parameter, register the mutation, action and getter, respectively. Take registration mutation as an example:
module.forEachMutation((mutation, key) = > {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})Copy the code
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler(local.state, payload)
})
}Copy the code
Find the array in the internal variable _mutations based on the name mutation. Then push the mutation’s return function into it. For example, there is such a mutation:
mutation: {
increment (state, n) {
state.count += n
}
}Copy the code
They’re going to put their callback in _mutations[increment].
commit
Mutation is put on the _mutations object. Next, the Store constructor initially places the Dispatch and commit of the Store class on the current instance. What is the execution of committing a mutation?
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]
if(! entry) {console.error(`[vuex] unknown mutation type: ${type}`)
return
}
this._withCommit((a)= > {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub= > sub(mutation, this.state))
if (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 method starts with unifyObjectStyle, because there are two ways to pass a parameter to commit:
store.commit('increment', {
amount: 10
})Copy the code
Another way to commit a mutation is to use an object that contains the type attribute directly:
store.commit({
type: 'increment'.amount: 10
})Copy the code
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
assert(typeof type === 'string'.`Expects string as the type, but found The ${typeof type}. `)
return { type, payload, options }
}Copy the code
If an object is passed in, the argument is converted. Then determine whether the mutation that needs commit has been registered, this._mutations[type], if not, the error is thrown. Each mutation callback in _mutations is then called through a loop. The subscribe callback function for each mutation is then executed.
Vuex helper function
Vuex provides four helper functions:
Using mapGetters as an example, look at the use of mapGetters:
The code is in SRC /helpers.js:
export const mapGetters = normalizeNamespace((namespace, getters) = > {
const res = {}
normalizeMap(getters).forEach(({ key, val }) = > {
val = namespace + val
res[key] = function mappedGetter () {
if(namespace && ! getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if(! (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
})
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 (typeofnamespace ! = ='string') {
map = namespace
namespace = ' '
} else if (namespace.charAt(namespace.length - 1)! = ='/') {
namespace += '/'
}
return fn(namespace, map)
}
}Copy the code
The normalizeNamespace method uses functional programming to receive a method and return a method. MapGetters take an array or an object as an argument:
computed: {
// Use the object expansion operator to mix getters into a computed object. mapGetters(['doneTodosCount'.'anotherGetter'.// ...])}Copy the code
mapGetters({
/ / this mapping. DoneCount for store. Getters. DoneTodosCount
doneCount: 'doneTodosCount'
})Copy the code
This is the case without passing a namespace, so let’s look at the actual implementation of this method. NormalizeNamespace starts with an argument jump, passing in an array or object with a namespace of “”, then fn(namespace, map) is executed, and then normalizeMap returns an array of the form:
{
key: doneCount,
val: doneTodosCount
}Copy the code
We then plug the method onto the res object to get an object of the following form:
{
doneCount: function() {
return this.$store.getters[doneTodosCount]
}
}Copy the code
This is what mapGetters originally wanted:
After the
by kaola/fangwentian