Vuex V3.0.1 is used for analysis
install
Vuex provides an install method to register vuue. Use. The install method determines the vue version.
// vuex/src/mixin.js
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)
}
}
Copy the code
For the 1.x version, vuexInit is added to vueInit. When vUE is initialized, vuex is also initialized. For the 2.x version, a beforeCreated hook function is added globally by mixin
The vuexInit method looks like this:
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
$store = this.$store = this.$store = this.$store = this. So you can get options.store. This refers to the current Vue instance. The options.store is an instance of the store object, so you can access the store instance in the component through this
const app = new Vue({
el: '#app'.// Provide the store object to the "store" option, which injects store instances into all child components
store,
components: { Counter },
template: '<div class="app"></div>'
})
Copy the code
Store
In the beforeCreate life cycle, it will get options.store, which will also be initialized
The core of every Vuex application is the Store, so there is a store initialization process. Here is a simple store example (from Vuex’s official website) :
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
Copy the code
The Store source code is located in vuex/ SRC /store.js. Within this class’s constructor, a new vue instance is created, so vuex can use many of vue’s features, such as responsive logic for data
When you initialize a Store, you initialize modules, Dispatch, commit, etc
Initialize the Module and build the Module tree
From the Store constructor constructor, the following is the initialization entry for modules:
this._modules = new ModuleCollection(options)
Copy the code
ModuleCollection is an ES6 class
// src/module/module-collection.js
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)}Copy the code
The constructor of this class calls the register method and the second argument, rawRootModule, is the options object passed in when the Store is initialized
// src/module/module-collection.js
register (path, rawModule, runtime = true) {
/ /... Omit irrelevant code
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0.- 1))
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)
})
}
}
Copy the code
The register method contains a new Module, which is a class used to describe a single Module. It defines data structs, attributes, and methods associated with a single Module, as follows:
These methods, attributes, and so on are related to the subsequent construction of the Module Tree
Since each module has its own state, Namespaced, actions, etc., these attributes and methods are also attached to each module object during module initialization. For example, here is the code for attaching state:
// src/module/module.js
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
Copy the code
If (rawModule. Modules) is passed as options. If (rawModule. Modules) is passed as options. So rawModule.modules is modules similar to those in ExampleA
/** * example code ExampleA */
const store = new Vuex.Store({
/ /... Omit irrelevant code
modules: {
profile: {
state: { age: 18 },
getters: {
age (state) {
return state.age
}
}
},
account: {
namespaced: true.state: {
isAdmin: true.isLogin: true
},
getters: {
isAdmin (state) {
return state.isAdmin
}
},
actions: {
login ({ state, commit, rootState }) {
commit('goLogin')}},mutations: { goLogin (state) { state.isLogin = ! state.isLogin } },// Nested modules
modules: {
// Further nested namespaces
myCount: {
namespaced: true.state: { count: 1 },
getters: {
count (state) {
return state.count
},
countAddOne (state, getters, c, d) {
console.log(123, state, getters, c, d);
return store.getters.count
}
},
actions: {
addCount ({ commit }) {
commit('addMutation')
},
delCount ({ commit }) {
commit('delMutation')
},
changeCount ({ dispatch }, { type } = { type: 1{})if (type === 1) {
dispatch('addCount')}else {
dispatch('delCount')}}},mutations: {
addMutation (state) {
console.log('addMutation1');
state.count = state.count + 1
},
delMutation (state) {
state.count = state.count - 1}}},// Inherits the parent module's namespace
posts: {
state: { popular: 2 },
getters: {
popular (state) {
return state.popular
}
}
}
}
}
}
})
Copy the code
So this is for modules, and if modules are present, then forEachValue is called to traverse modules
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key= > fn(obj[key], key))
}
Copy the code
Register all modules that exist in Modules, where the key is the name of each module, such as profile and account in ExampleA
When we call this.register again, path.length === 0 is not true, so else logic is used, and we get this.get method:
// src/module/module-collection.js
get (path) {
return path.reduce((module, key) = > {
return module.getChild(key)
}, this.root)
}
Copy the code
The path is iterated, and then the iterated item is called getChild. This getChild is a method in the previous Module class that gets submodule objects by key, by Module name, in the current Module. The corresponding method is addChild, Add a submodule to the current module, that is, establish the relationship between the parent and child:
// src/module/module.js
this._children = Object.create(null)
// ...
addChild (key, module) {
this._children[key] = module
}
// ...
getChild (key) {
return this._children[key]
}
Copy the code
The following operations are used to iterate over all Modules and their submodules using the module name as the property key to form a Modules Tree with this. Root as the vertex.
Install the module tree
With the Module tree built above, it’s time to install the tree
// src/store.js
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
Copy the code
I’m doing a bunch of things in this method, one by one
If you find that the current Module has the namespaced attribute and its value is true, you will register it in the Namespace map.
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
Copy the code
The getNamespace method is a method on the ModuleCollection class that concatenates the full namespace of the current module according to its path
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) = > {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : ' ')},' ')}Copy the code
Call getNamespace to get the namespace name, then use the namespace name as the key, and cache the module object to the corresponding namespace as a value on store._modulesNamespaceMap. This is available via this.$store._modulesNamespaceMap, for example, for the example in ExampleA:
Next is a judgment logic, accord! isRoot && ! The hot condition is executed, where isRoot is defined at the beginning of the installModule method:
constisRoot = ! path.lengthCopy the code
Path is the state of the parent-child relationship maintained by the Module tree. If == 0, isRoot = true; if == 0, isRoot = true; if == 0, isRoot = true
InstallModule = []; path.length === 0
Set up the state
// src/store.js
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 called:
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) = > state[key], state)
: state
}
Copy the code
In this case, the state of the final submodule is actually found through layers of path.reduce
For example, for state under account/myCount, whose path is [‘account’, ‘myCount’], the global state structure is as follows:
{
profile: {... },account: {
isAdmin: true.isLogin: true.// This is the namespace of the submodule myCount
myCount: {
// This is the state of the submodule myCount
count: 1
},
posts: {
popular: 2}}}Copy the code
When the getNestedState method is called on the global state and path = [‘account’, ‘myCount’], it will end up with the state of /myCount:
{
count: 1
}
Copy the code
After finding the state of the specific submodule, mount it to store._withCommit
Building the local Context
The makeLocalContext method is then executed:
const local = module.context = makeLocalContext(store, namespace, path)
Copy the code
A general description of what this method does is given in its comments:
/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */
function makeLocalContext (store, namespace, path) {
// ...
}
Copy the code
Local Dispatch, commit, getter, state, and, if no namespace exists, mount it directly to the root Module
The namespace module is called dispatch, COMMIT, getters, and state. If the module uses a namespace, the namespace will be appended to the path automatically
For example, for dispath, if the current module has a namespace, the module’s dispatch method is called and the namespace is concatenated to type, and then the method on store is found based on the concatenated type and executed:
// makeLocalContext
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(process.env.NODE_ENV ! = ='production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return}}return store.dispatch(type, payload)
}
Copy the code
For ExampleA code, for example, want to change the account/myCount count value, can be directly call global enclosing $store. Dispatch (‘ account/myCount/changeCount ‘), When type = 1, dispatch(‘addCount’) is executed. This dispatch is used to execute actions for account/myCount. Instead of addCount in root Module
Therefore, a full path type concatenation is performed here. The namespace of the current module is concatenated with type, that is, account/myCount/ and addCount are concatenated to account/myCount/addCount. We pass the full path type as an argument to the store.dispatch method, which simplifies the concatenation of nested Module paths
The logic for commit is similar, but the getter and state are a little different
// src/store.js
// makeLocalContext
Object.defineProperties(local, {
getters: {
get: noNamespace
? (a)= > store.getters
: (a)= > makeLocalGetters(store, namespace)
},
state: {
get: (a)= > getNestedState(store.state, path)
}
})
Copy the code
For getters, if there is no namspace, return store.getters directly, otherwise makeLocalGetters is called:
// src/store.js
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type= > {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) ! == namespace)return
// extract local getter type
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: (a)= > store.getters[type],
enumerable: true})})return gettersProxy
}
Copy the code
It might not be very clear to look at this code directly, so here’s an example. For example, for the getter account/myCount/count (type in the source code above), its namespace is Account /myCount/, Its localType is count, and when accessing getters gettersproxy. count, it automatically points to the global account/myCount/count
And then state, which calls getNestedState, which is basically the same as the above method, but I won’t go into that
In addition, object.defineProperty is used several times to set the get function to attributes on the Object, rather than assigning values to attributes such as localType above. The purpose of this method is clearly commented in the code. In order to be able to calculate the value at the time of access, which not only reduces the real-time computation, but also ensures that the value obtained is real-time and accurate. This is related to the responsive mechanism of VUE, which will not be discussed here
The makeLocalContext method is a global dispatch, commit, getter, state mapping of a namespace submodule:
The vuex website introduces the Actions section with this quote:
Where the Action function takes a context object with the same methods and properties as the Store instance
Register mutation Action getters
Mutation
First, Mutation:
// src/store.js
module.forEachMutation((mutation, key) = > {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
Copy the code
Mutations are mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations, mutations
// src/store.js
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
Copy the code
The method adds wrappedMutationHandler to _mutations[types] on the root store. The value of store._mutations[type] is an array, which means that _mutations of the same type can correspond to multiple wrappedMutationHandler methods
For example, for the account/myCount module in ExampleA, if its namespaced attribute doesn’t exist, or its value is false, that is, it doesn’t have a separate namespace, and then mutations has a method called goLogin, Mutations [‘account/goLogin’] have two entries in the array, one is the goLogin method under account, One is the goLogin method under Account /myCount
This is not the case if namespaced for Account /myCount, since its goLogin type is Account /myCount/goLogin
action
// src/store.js
module.forEachAction((action, key) = > {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
Copy the code
The Mutation does not delete all actions from the store, but does not delete all actions from the store. Actions are stored on the store. It also has to do with namespaces
getter
// src/store.js
module.forEachGetter((getter, key) = > {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
Copy the code
Getters are the same logic, going through all getters and then hanging onto a store property, except that the getter is hanging on store._wrappedgetters. Additionally, only one value is allowed for the same key, and if multiple values exist, The second shall prevail:
// src/store.js
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._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters)}}Copy the code
Finally, if the current module has submodules, it iterates through all of its submodules and executes the installModule method on those submodules, which repeats the above steps again
This completes the installModule method. When you call installModule, there are two lines of comment:
// src/store.js
// 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
It means:
Initializing root Modules also registers all child modules recursively and collects getters for all modules to this._wrappedgettersCopy the code
InstallModule is an initialization for state, getters, actions, mutations for all modules, including submodules
Initialize the Store VM
Next, resetStoreVM is executed:
// src/store.js
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
Copy the code
The function of this method can be seen roughly from its comments. Initialize the Store VM. When we look at this VM we should think of the vue instance VM
And registers _wrappedGetters as a computed property, which is a collection of getters for each module, as mentioned earlier, One of the features of computed properties in VUE is that computed properties are cached based on their dependencies. They are reevaluated only when the dependencies change, which means efficient real-time calculation. Here we want getters of each module on store to have this feature as well
// src/store.js
// resetStoreVM
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
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
Run this through _wrappedGetters using forEachValue, which was also mentioned earlier, so fn(store) here is essentially this:
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters)}Copy the code
WrappedGetter returns the result of a rawGetter. The rawGetter can be seen as the result of a getter’s calculation, so the four parameters we get in the parameters of the getter method refer to the above four:
// https://vuex.vuejs.org/zh/api/#getters
state, // Local state of the module if defined in the module
getters, // Equivalent to store.getters
rootState // Equivalent to store.state
rootGetters / / all getters
Copy the code
Once you get the getter, you give it to computed
We then define an Object.defineProperty:
/ / SRC, store. Js
Object.defineProperty(store.getters, key, {
get: (a)= > store._vm[key],
enumerable: true // for local getters
})
Copy the code
Store. getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; store.getters[key] = store._vm[key]; It has to do with this logic:
/ / SRC, store. Js
store._vm = new Vue({
data: {
? state: state
},
computed
})
Copy the code
Store._vm is really just an instance of VUE that has only data and computed attributes, just to take advantage of vUE’s responsive mechanism
There is a mapping between state and getter, because the calculation result of getter must depend on state, and there must be a correlation between them. The Store class has a accessor property for state:
// src/store.js
get state () {
return this._vm._data.? state }Copy the code
The state to getter mapping flow is as follows:
Here is a logic to standardize the development approach:
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
Copy the code
Strict is explicitly declared when the developer initialses the store. It seems that most people don’t care about this, but in order to comply with vuex’s development specification, it is better to add this property
The enableStrictMode method is as follows:
// src/store.js
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.? state }, () => {if(process.env.NODE_ENV ! = ='production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)}}, {deep: true.sync: true})}Copy the code
As mentioned above, store._vm is actually a vue instance, so it has a $watch method that checks this._data.? Case of state (” state “), store. _research must be true (” right “)
Research (” store. _research “) indicates the value defined in the store initialization code (” store “), which defaults to false:
// src/store.js
this._committing = false
Copy the code
This value is modified in the _withCommit method:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
Copy the code
“Make sure this. __research is true (right) when fn is executed and reset (right),” this _withCommit scenario usually changes state (commit) :
// src/store.js
commit (_type, _payload, _options) {
// omit extraneous code
// ...
this._withCommit((a)= > {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// omit extraneous code
// ...
}
Copy the code
EnableStrictMode is used to prevent the state value from being illegally modified by vuEX methods, such as commit and replaceState, from being warned in the development environment
conclusion
From the above analysis, the initialization of VUex is basically closely related to the initialization of Store. After the initialization of Store, vuex is basically initialized, but there are still many parts involved in the process
Up to now, all the analysis is about initialization. Vuex’s API has hardly been mentioned, and vuex’s ability is reflected through THE API. Let’s analyze vuex API again at some time