This article is mainly to disentangle vuex v2.x source code, quickly understand the implementation principle of VUex 1.0 and VUex 2.0.
Reading readiness
Vuex’s source code is based on es6 syntax, so you need to know the basics before reading this article:
1.Es6 grammar
2.Vuex official document, like me poor English reading ability, andChinese document
3. Front-end basic JS, CSS, HTML needless to say, vUE basic usage
Download the source code
3.* is written in TS syntax, so it is relatively more expensive to read. I am not finished reading vuex3.x source code.
Select a tag on Github and clone it locally to v2.5.0
The source code interpretation
Initialize the
Where to start: index.js
The starting point for reading is currently index.js. Here’s what index.js does:
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
Copy the code
Let’s take a look at them: Store Vuex provides a state store class that is used in projects as a state manager. MapState creates computed properties for the component to return the state in the VUex store. MapMutations creates the component method to submit mutation MapGetters creates computed properties for components to return the return value of getters mapActions creates component methods to distribute action createNamespacedHelpers create namespace-based component binding helpers and then, I will explain how VUEX works in the order vuex is used in vUE projects, and then expand on other attributes and methods of VUex
Step 1: Vuue. Use (vuex)
Why not start with store? This is the first step vuex inserts into a vue project
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 applyMixin(Vue) }Copy the code
The code looks pretty simple. What does Install do: 1. Check if there is a global Vue (window.vue), if not, assign a value to ensure that our vuex is installed only once. Vue.use(vuex) is a vuex install method. Then mount the vue to the global object so that it can be accessed elsewhere. Pull the last line of applyMixin to see the code:
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ 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)
}
}
function vuexInit () {
const options = this.$options
// store injection
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
Determine the vUE version number and execute a break code before the vue life cycle initialization hook (init for vue1.0, beforeCreated for vue2.*). The specific execution code is in vuexInit. Add a store to the vue instance and check if there is a store in this. Options. If there is a store in this. Don’t bother with this.options.store or this.options.parent.$store.
Vue uses the second vuex step, new vuex.store
To instantiate a store, let’s look at what the store constructor does:
export class Store { constructor (options = {}) { 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.`) } ... }Copy the code
Make sure to install, and then some assertion functions, but js does not have this assertion function, just mock assertion function. This is done by assigning some attributes and pointing this of the class methods dispatch and commit to the current store instance
Let’s continue:
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
Copy the code
These three lines of code do something important. The resetStoreVM method initializes store._vm, observing state and getters. Plugins use the plugins passed in to view the details one by one
#### installModule function installModule (store, rootState, path, module, hot) { const isRoot = ! path.length const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { store._modulesNamespaceMap[namespace] = module } // set state if (! isRoot && ! hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } 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 type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, 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
Store indicates the current store instance rootState indicates the rootState path indicates the path of the currently nested module array module indicates the currently installed module hot this parameter is true when dynamically changing modules or hot updating
The next step is to register the installation of the options passed in for initialization. The specific implementation will be explained in the later article. We will first understand what it is. The registration of module is worth paying attention to:
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
Copy the code
Because store is a single state tree, it becomes difficult to maintain if more and more data needs to be managed, so module is introduced to make its structure more standardized and easy to maintain.
resetStoreVM
After executing installModule, execute resetStoreVM
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = () => 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
store._vm = new Vue({
data: {
?state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.?state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
Copy the code
Store is given a private variable, _vm, which is an instance of Vue. This _VM object will retain our state tree and store store getters as computed properties.
Vuex initialization summary
The main core of Vuex initialization is the installModule and resetStoreVM functions. Through the registration of mutations, Actions and getters, the state is divided by modules, forming a state tree according to the nesting of modules. Actions, mutations and getters are global. Now that we have a general understanding of vuex initialization, let’s take a look at the API we used in the project and explain how it was implemented. We will also clarify some of the things that were not explained during vuex initialization.
Vue API,
commit
Let’s look at the commit function definition:
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) { if (process.env.NODE_ENV ! == 'production') { console.error(`[vuex] unknown mutation type: ${type}`) } return } this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) 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
dispatch
Let’s take a look at the function definition for Dispatch:
dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } const entry = this._actions[type] if (! entry) { if (process.env.NODE_ENV ! == 'production') { console.error(`[vuex] unknown action type: ${type}`) } return } this._actionSubscribers.forEach(sub => sub(action, this.state)) return entry.length > 1 ? Promise.all(entry.map(handler => Handler (payload))) : Entry [0](payload)} Parameters passed in: _type action type _payload The value we want to updateCopy the code
subscribe
So let’s look at the function definition of subscribe
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
Copy the code
watch
watch (getter, cb, options) { if (process.env.NODE_ENV ! == 'production') { assert(typeof getter === 'function', `store.watch only accepts a function.`) } return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options) }Copy the code
Getter listener cb callback options Optional. Configuration responses such as {deep: true} receive a getter return value and call the callback function when the value changes. The getter accepts state as the only argument and options as the _vm.watch argument.