Vuex source code analysis
Understand vuex
What is a vuex
Vuex is a state manager for unified state management for VUE, which is mainly divided into state, getters, mutations and Actions.
Vue components are rendered based on state, and the re-rendering of components is triggered when state changes, and getters are derived based on the responsive principle of VUE.
Getters constructs different forms of data based on state. When state changes, it changes in response. The state of
Changes can only be triggered by a COMMIT, and each change is logged by DevTools. Asynchronous actions are triggered by actions, such as sending background API requests,
When the asynchronous operation is complete, the value is obtained and the mutations event is triggered, which in turn reevaluates the STAT and triggers the view to be rerendered.
Why vuex
- To solve the communication between components and the traditional event mode is too long to debug the call chain. In the use of VUE, we use the event mode provided by VUE to realize the communication between father and son, or use eventBus to carry out multi-components
Between traffic, but as the project becomes huge, invocation chain sometimes is long, will be unable to locate to the sponsor of the event, and debugging is based on the pattern of events will let developers A headache, the next person to take over project is hard to know what are the effects to trigger an event, vuex state layer and view layer to pull away, All states are managed uniformly and all components share a single state. With Vuex we move our focus from events to data. We can only focus on which components refer to a value in a state. In addition, communication between components becomes much easier by moving from subscribing to the same event to sharing the same data.
- To solve the problem of data transmission between parent and child components, in the development of VUE, we will use props or inject to realize data transmission between parent and child components, but when the component hierarchy is too deep
Some components that don’t need specific data inject unnecessary data for data delivery. The data delivery of inject is inherently flawed when the code is filled with provided and inject. Don’t know where the component Inject data is provided. Vuex takes some common data and manages it in a unified way, making this complex data transfer effortless.
The install
To implement introducing vuex through the vue.use () method, you need to define an install method for vuex. The main function of the Intall method in VUEX is to inject the Store instance into each VUE component as follows
Export function install (_Vue) {if (Vue && Vue === _Vue) {console. Warn ("duplicate install"); } Vue = _Vue; // Start registering global mixins applyMixin(Vue); }Copy the code
The above code avoids duplicate installation by defining a global variable Vue to hold the current imported Vue, and then injecting store into each instance through the apllyMixin implementation
Export default function (Vue) {const version = Number(Vue.version.split(".")[0]); If (version >= 2) {Vue. Mixin ({beforeCreate: vuexInit}); } else {// lower version, put the initialization method in options.init const _init = vue.prototype. _init; Vue.prototype._init = function (options = {}) { options.init = options.init ? [vuexInit].concat(options.init) : vuexInit; _init(); }; } function vuexInit () {// Root component if (this.$options && this.$options.store) {this.$store = typeof this.$options.store === "function" ? this.$options.store() : this.$options.store; } else if (this.$options.parent && this.$options.parent.$store) {// Non-root component this. }}}Copy the code
$options (new Vue(options:Object) when the root instance is instantiated)
new Vue({
store
})Copy the code
Contains the store attribute, and if so, this of the instance.Options. store, if not, point to this.The store.
After installing, we can point all the child components’ $store property to this store by passing store to Options when instantiating the root component.
In addition, vueInit will be executed when applyMixin is executed to determine the current Vue version number. After version 2, vueInit will be executed when all components are instantiated by mixin
For versions 2 and below, the injection is performed by insertion in options.init. The following are some summary of the installation functions
- Avoid repeated installation
- The initial method is injected in different ways according to the version. The initial method is injected through options.init before 2 and mixin after 2
- Inject store into the instance property $store of all vues
How to implement a simple COMMIT
Commit is actually a simple publish-subscribe implementation, but it involves module implementation, reactive implementation between state and getters, and lays the groundwork for actions later
use
First, review the use of COMMIT
Open mutations = new vuex. store ({state: {count: 1}, mutations: { add (state, number) { state.count += number; }}});Copy the code
When a store is instantiated, the mutation parameter is an event in the event queue. Each event is passed two parameters, namely, state and payload, and each event realizes changing the value of state according to the payload
<template> <div> count:{{state.count}} <button @click="add">add</button> </div> </template> <script> export default { name: "app", created () { console.log(this); }, computed: { state () { return this.$store.state; } }, methods: { add () { this.$store.commit("add", 2); }}}; </script> <style scoped> </style>Copy the code
We trigger the appropriate type of mutation in the component with a commit and pass in a payload, where the state changes in real time
implementation
First, what do we need to do in the constructor to implement commit
This._mutations = object.create (null); this._modules = new ModuleCollection(options); // Declare the publishing function const store = this; const { commit } = this; this.commit = function (_type, _payload, _options) { commit.call(store, _type, _payload, _options); }; const state = this._modules.root.state; // Install the root module this.installModule(this, state, [], this._modules. This.resetstorevm (this, state); }Copy the code
The first is the three instance properties _mutations is an event queue in publish subscribe mode, and the _modules property encapsulates the passed options:{state, getters, mutations, Actions} to provide some basic operation methods. The commit method fires the corresponding event in the event queue. We will then register the event queue in installModule and implement a responsive state in resetStoreVm.
modules
In instantiation store when we pass in an object parameter, which contains the state, mutations, the actions, getters, modules such as data items we need to encapsulate the data items, and exposed a this some of the operating method of a data item, this is the role of the Module class, In addition, vuEX has modules division, which needs to be managed, and thus derived ModuleCollection class. This section first focuses on the implementation of COMMIT, and module division will be discussed later. For the directly passed state, mutations, actions, Getters, in Vuex, are wrapped in the Module class and registered in the Root property of the ModuleCollection
export default class Module { constructor (rawModule, runtime) { const rawState = rawModule.state; this.runtime = runtime; // 1. Todo: This._rawModule = rawModule; this.state = typeof rawState === "function" ? rawState() : rawState; Mutations) {forEachValue(this._rawmodule. Mutations) {mutations (this._rawmodule. Mutations, fn); } } } export function forEachValue (obj, fn) { Object.keys(obj).forEach((key) => fn(obj[key], key)); }Copy the code
The rawModule parameters passed into the constructor are {state, mutations, actions, Getters} object, defined in the Module class two attributes _rawModule to hold the incoming rawModule, forEachMutation mutations traverse the execution, mutation object of value, the key to fn and perform, Next, attach this Module to the Root property of the Modulecollection
Export default class ModuleCollection {constructor (rawRootModule) {// path,module,runtime this.register([], rawRootModule, false); } // 1. What does todo Runtime do? register (path, rawRootModule, runtime) { const module = new Module(rawRootModule, runtime); this.root = module; }}Copy the code
After all this wrapping, the this._modules property is the following data structure
state
Since all events saved in mutations are to change state according to certain rules, we will first introduce how Store manages state, especially how to change the value of getters in response by changing state. One method mentioned in the constructor, resetStoreVm, implements the reactive relationship between state and getters
resetStoreVm (store, state) { const oldVm = store._vm; _vm = new Vue({data: {? state: state } }); If (oldVm) {vue.nexttick (() => {oldVm. Destroy (); }); }}Copy the code
This function takes two arguments, instance itself and state, and first registers a vue instance stored on the Store instance attribute _VM, where the data item is defined
export class Store {
get state () {
return this._vm._data.?state;
}
set state (v) {
if (process.env.NODE_ENV !== "production") {
console.error("user store.replaceState()");
}
}
}Copy the code
It is important to note that we cannot assign state directly, but rather through store.replacEstate, otherwise an error will be reported
Event registration
The publish-subscribe model consists of two steps: event subscription and event publishing. How does Vuex implement the subscription process
This._mutations = object.create (null); // Null this._modules = new ModuleCollection(options); const state = this._modules.root.state; // Install the root module this.installModule(this, state, [], this._modules. } installModule (store, state, path, module) {const local = this.makelocalContext (store, path); module.forEachMutation((mutation, key) => { this.registerMutation(store, key, mutation, local); }); // mutation registerMutation (store, type, handler, local) { const entry = this._mutations[type] || (this._mutations[type] = []); entry.push(function WrappedMutationHandler (payload) { handler.call(store, local.state, payload); }); }}Copy the code
We only intercept relevant parts of the code, including two key methods installModule and registerMutation. We will omit some parts about module encapsulation here. Local here can be simply understood as a {state, getters} object. The general process of event registration is to traverse mutation and wrap it and push it into the event queue of the specified type. The mutation is first traversed through forEachMutation, an instance method of the Moulde class. In addition, registerMutation is performed to register the event, and an event queue of the specified type of this._mutations is generated in registerMutation. The data structure of this._mutations after the registered event is as follows
Event publishing
Based on the structure of this._mutations after event registration, we can easily implement event release, find the event queue of the specified type, traverse the queue, pass in parameters, and execute them.
Const {type, payload} = unifyObjectStyle(_type, payload, _options) { _payload, _options); const entry = this._mutations[type]; ForEach (function commitIterator (handler) {handler(payload); }); }Copy the code
But it’s important to note that the parameters need to be handled first, which is what unifyObjectStyle does
// Add parameters: Function unifyObjectStyle (type, payload, options) {if (isObject(type)) {payload = type; options = payload; type = type.type; } return { type, payload, options }; }Copy the code
It can be either a string or an object, and when it’s an object, the internal type is type.type, and the second parameter becomes type, and the third parameter becomes payload. At this point, the principle of commit is covered, and all the code can be found at the branch github.com/miracle931….
Action and Dispatch principles
usage
Define an Action
add ({ commit }, number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const pow = 2;
commit("add", Math.pow(number, pow));
resolve(number);
}, 1000);
});
}Copy the code
Trigger action
this.$store.dispatch("add", 4).then((data) => {
console.log(data);
});Copy the code
Why action is needed
Sometimes we need to trigger an event that executes asynchronously, such as an interface request, but if we rely on a synchronized event queue like Mutatoin, we can’t get the final state of the execution. At this point we need to find a solution to achieve the following two goals
- A queue that executes asynchronously
- Capture the final state of asynchronous execution
Through these two goals, we can roughly calculate how to achieve it. As long as we ensure that all events defined return a promise, and then put these promises in a queue and execute them through promise.all, a promise of final state will be returned, which can not only ensure the execution order of events, It can also capture the final execution state.
Implementation of Action and Dispatch
registered
First we define an instance property, _Actions, to hold the event queue
constructor (options = {}) {
// ...
this._actions = Object.create(null);
// ...
}Copy the code
We then define an instance method, forEachActions, in the Module class to iterate through the actions
export default class Module { // ... forEachAction (fn) { if (this._rawModule.actions) { forEachValue(this._rawModule.actions, fn); }} / /... }Copy the code
Then, during the installModule phase, go through the Actions and register the event queue
installModule (store, state, path, module) {
// ...
module.forEachAction((action, key) => {
this.registerAction(store, key, action, local);
});
// ...
}Copy the code
registered
registerAction (store, type, handler, local) { const entry = this._actions[type] || (this._actions[type] = []); entry.push(function WrappedActionHandler (payload, cb) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, state: local.state, rootState: store.state }, payload, cb); // The default action returns a promise. If not, wrap the return value in the promise if (! isPromise(res)) { res = Promise.resolve(res); } return res; }); }Copy the code
The registration method contains four parameters: store for store instance, type for Action type, and handler for action function. First check whether there is an event queue of this type acion. If not, initialize it to an array. The event is then pushed to the event queue of the specified type. Two things to note: first, the action function calls a context object as its first argument, and second, the event always returns a promise.
release
dispatch (_type, _payload) { const { type, payload } = unifyObjectStyle(_type, _payload); / /?? Action const entry = this._actions[type]; action const entry = this._actions[type]; // Return promise,dispatch().then() accepts an array or some value return entry.length > 1? Promise.all(entry.map((handler) => handler(payload))) : entry[0](payload); }Copy the code
First get the event queue of the corresponding type, then pass in the parameter execution, return a promise, when the number of events contained in the event queue is greater than 1, save the returned promise in an array, and then trigger through pomise.all, If there is only one event in the event queue, then we can get the result of asynchronous execution via dispatch(type, payload). Then (data=>{}). In the event queue, events are triggered by promise.all. Both objectives have been achieved.
Getters principle
The use of the getters
In store instantiation we define the following options:
const store = new Vuex.Store({ state: { count: 1 }, getters: { square (state, getters) { return Math.pow(state.count, 2); } }, mutations: { add (state, number) { state.count += number; }}});Copy the code
First, we define a state, getters, and mutations in store, where state contains a count with an initial value of 1, getters defines a square that returns the square of count, and mutations defines an add event, Count increases the number when add is triggered. Then we use the store in the page:
<template> <div> <div>count:{{state.count}}</div> <div>getterCount:{{getters.square}}</div> <button @click="add">add</button> </div> </template> <script> export default { name: "app", created () { console.log(this); }, computed: { state () { return this.$store.state; }, getters () { return this.$store.getters; } }, methods: { add () { this.$store.commit("add", 2); }}}; </script> <style scoped> </style>Copy the code
The result of this execution is that every time we fire the Add event, state.count increases by 2, and the getter always squares state.count. This reminds us of the relationship between data and computed in VUE, which vuex uses in fact.
The realization of the getters
Start by defining an instance property _wappedGetters to store getters
export class Store { constructor (options = {}) { // ... this._wrappedGetters = Object.create(null); / /... }}Copy the code
Define an instance method to iterate over getters in Modules, register getters in the installModule method, and store it in the _wrappedGetters property
installModule (store, state, path, module) {
// ...
module.forEachGetters((getter, key) => {
this.registerGetter(store, key, getter, local);
});
// ...
}Copy the code
registerGetter (store, type, rawGetters, Local) {// Handle getter duplicates if (this._wrappedgetters [type]) {console.error("duplicate getter"); } // set _wrappedGetters, This._wrappedgetters [type] = function wrappedGetterHandlers (store) {return rawGetters(local.state, local.getters, store.state, store.getters ); }; }Copy the code
Note that vuex cannot define two getters of the same type. At registration time, we pass in a function that returns the result of the execution of the option getters as a store instance. Getters in the option accepts four parameters: state and getters in scope and store instance. The problem of local will be introduced later in module principle. In this implementation, the parameters of local and store are consistent. Then we need to inject all getters into computed during resetStoreVm, and when we access an attribute in getters, we need to delegate it to the corresponding attribute in store.vm
// Register a responsive instance resetStoreVm (store, state) {// Point store.getters[key] to store._vm[key],computed assignment forEachValue(wrappedGetters, function (fn, key) { computed[key] = () => fn(store); }); _vm = new Vue({data: {? state: state }, computed }); If (oldVm) {vue.nexttick (() => {oldVm. Destroy (); }); }}Copy the code
During the resetStroreVm period, walk through wrappedGetters, wrap getters in a computed with the same key, and inject this computed into the store._VM instance.
resetStoreVm (store, state) {
store.getters = {};
forEachValue(wrappedGetters, function (fn, key) {
// ...
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
});
});
// ...
}Copy the code
Then point the properties in store.getters to the corresponding properties in store._vm, which is store.puted so that when the data.? State (store.state) in store._vm changes, Getters that refer to state are also evaluated in real time. That’s how getters respond to changes. See github.com/miracle931.
Principle of helpers
Helpers. Js exposed the outward in the four methods, respectively mapState, mapGetters, mapMutations and mapAction. These four helper methods help developers quickly reference their own defined state,getters,mutations, and actions in their components. Learn how to use it first and then how it works
const store = new Vuex.Store({ state: { count: 1 }, getters: { square (state, getters) { return Math.pow(state.count, 2); } }, mutations: { add (state, number) { state.count += number; } }, actions: { add ({ commit }, number) { return new Promise((resolve, reject) => { setTimeout(() => { const pow = 2; commit("add", Math.pow(number, pow)); resolve(number); }, 1000); }); }}});Copy the code
So that’s our definition of store
<template> <div> <div>count:{{count}}</div> <div>getterCount:{{square}}</div> <button @click="mutAdd(1)">mutAdd</button> <button @click="actAdd(1)">actAdd</button> </div> </template> <script> import vuex from "./vuex/src"; export default { name: "app", computed: { ... vuex.mapState(["count"]), ... vuex.mapGetters(["square"]) }, methods: { ... vuex.mapMutations({ mutAdd: "add" }), ... vuex.mapActions({ actAdd: "add" }) } }; </script> <style scoped> </style>Copy the code
Store is then introduced into the component and used by mapXXX. By looking at the way these methods are referenced, you can see that each of these methods eventually returns an object whose values are all functions, and that these methods are injected into the computed and methods properties by expanding the operator. For mapState and mapGetters, return a function in an object that returns the value of the passed argument (return store.state[key]; Or return store.getters[key]), and for mapMutations and mapActions, return a function in an object, which executes commit ([key],payload), Or dispatch ([key],payload). This is the simple principle of these methods. We will look at vuEX implementations one by one
MapState and mapGetters
Export const mapState = function (states) {// define a result map const res = {}; State normalizeMap(states).foreach (({key, Function mappedState () {const state = this.$store. State; const getters = this.$store.getters; return typeof val === "function" ? val.call(this, state, getters) : state[val]; }; }); Return res; };Copy the code
First, the final return value of mapsState is an object, and the parameters we pass in are the properties that we want to map out. MapState can be passed in either an array of strings that contain the referenced properties, or an array of objects that contain the mapping between values and references. These two forms of parameter passing, We need to normalize with normalizeMap to return a uniform array of objects
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}Copy the code
The normalizeMap function first checks whether the value passed in is an array. If it is, it returns an array of objects in which both key and val are array elements. If it is not an array, the normalizeMap function determines that the value passed in is an object. After normalizeMap, the map will be an array of objects. It then iterates through the normalized array and assigns to the returned object. The assignment function returns the key corresponding to state. If the value passed in is a function, getters and state are passed in and executed, and the object is returned. This allows you to reference the value of state directly through key when expanded in computed properties. The implementation principle of mapGetters and mapState is basically the same
export const mapGetters = function (getters) {
const res = {};
normalizeMap(getters)
.forEach(({ key, val }) => {
res[key] = function mappedGetter () {
return this.$store.getters[val];
};
});
return res;
};Copy the code
MapActions and mapMutations
export const mapActions = function (actions) { const res = {}; normalizeMap(actions) .forEach(({ key, val }) => { res[key] = function (... args) { const dispatch = this.$store.dispatch; return typeof val === "function" ? val.apply(this, [dispatch].concat(args)) : dispatch.apply(this, [val].concat(args)); }; }); return res; };Copy the code
MapActions also returns an object whose key is referenced in the component and whose value is a function that takes the payload during the execution of dispatch. The action is triggered by dispath(actionType,payload). If the parameter is a function, the dispatch and payload are passed as parameters and executed. This makes it possible to call multiple actions in combination with mapActions, or to customize some other behavior. This object is eventually returned, and when expanded in the component’s Methods property, the action can be triggered by calling the function corresponding to the key. MapMutation is implemented in much the same way as mapActions
export const mapMutations = function (mutations) { const res = {}; normalizeMap(mutations) .forEach(({ key, val }) => { res[key] = function mappedMutation (... args) { const commit = this.$store.commit; return typeof val === "function" ? val.apply(this, [commit].concat(args)) : commit.apply(this, [val].concat(args)); }; }); return res; };Copy the code
module
In order to facilitate the segmentation of different functions in Store, different functions can be assembled into a separate module in VUEX. State can be separately managed inside the module and global state can be accessed.
usage
// main.js const store = new Vuex.Store({ state: {}, getters: {}, mutations: {}, actions: {}, modules: { a: { namespaced: true, state: { countA: 9 }, getters: { sqrt (state) { return Math.sqrt(state.countA); } }, mutations: { miner (state, payload) { state.countA -= payload; } }, actions: { miner (context) { console.log(context); }}}}});Copy the code
//app.vue <template> <div> <div>moduleSqrt:{{sqrt}}</div> <div>moduleCount:{{countA}}</div> <button @click="miner(1)">modMutAdd</button> </div> </template> <script> import vuex from "./vuex/src"; export default { name: "app", created () { console.log(this.$store); }, computed: { ... vuex.mapGetters("a", ["sqrt"]), ... vuex.mapState("a", ["countA"]) }, methods: { ... vuex.mapMutations("a", ["miner"]) } }; </script> <style scoped> </style>Copy the code
In the code above, we define a module with key A, whose namespaced is true, and for modules with namespace=false, it automatically inherits the parent module’s namespace. For module A, it has the following features
- Have your own separate state
- Getters and Actions can access state,getters, rootState, rootGetters
- Mutations can only change the state in the module
Based on the above features, the subsequent Module implementation can be divided into several parts
- What data format will be used to store the Module
- How do I create a module context that encapsulates state, commit, dispatch, and getters, and make the commit change only the internal state, and make the module’s context change only the internal state
Getters, Dispatch keeps the root module accessible
- How to register getters, mutations, actions in the module and bind them to namespace
- How do helper methods find getters, Mutations, and actions in a namespace and inject them into the component
Construct a nested module structure
The final module constructed by VUex is such a nested structure
The first level is a root, and each level after that has an _rawModule and _children property that holds its own getters, Mutations, actions and, respectively
The child. Implementing such a data structure can be done with a simple recursion
The first is our input parameter, which looks something like this
{
state: {},
getters: {},
mutations: {},
actions: {},
modules: {
a: {
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {}
},
b: {
namespaced: true,
state: {},
getters: {},
mutations: {},
actions: {}
}
}
}Copy the code
We will use this object as an argument to the ModuleCollection instantiation in the store constructor
export class Store { constructor (options = {}) { this._modules = new ModuleCollection(options); }}Copy the code
All the construction of nested structures takes place during the Instantiation of the ModuleCollection
// module-collection.js export default class ModuleCollection {constructor (rawRootModule) { path,module,runtime this.register([], rawRootModule, false); } get (path) {return path.reduce((module, key) => module.getChild(key), this.root); } // 1. What does todo Runtime do? Register (path, rawModule, Runtime = true) {// Generate Module const newModule = newModule (rawModule, Runtime); If (path.length === 0) {// Root module, register on root this.root = newModule; } else {// Attach const parent = this.get(path.slice(0, -1)); parent.addChild(path[path.length - 1], newModule); } // Whether the module contains submodules, If (rawModule.modules) {forEachValue(rawModule.modules, (newRawModule, key) => { this.register(path.concat(key), newRawModule, runtime); }); }}}Copy the code
// module.js export default class Module { addChild (key, module) { this._children[key] = module; }}Copy the code
In the register function, an instance of a Module is created based on the rawModule passed in. Then, the registered path is used to determine whether the Module is the root Module. If so, the Module instance is mounted to the root attribute. If not, find the parent module of the module through get method, mount it to the _children attribute of the parent module through addChild method of the module, and finally determine whether the module contains nested modules. If so, traverse nested modules and recursively execute register method. This allows you to construct the nested module structure shown above. With the above structure, we can use reduce method to obtain the modules under the specified path through path, and we can also use recursive way to carry out unified operation for all modules, which greatly facilitates module management.
Tectonic localContext
With the basic module structure in place, the next question is how to encapsulate the module scope so that each module has its own state and methods for managing that state, and we want those methods to have access to global properties as well. To sum up what we’re going to do now,
// module
{
state: {},
getters: {}
...
modules:{
n1:{
namespaced: true,
getters: {
g(state, rootState) {
state.s // => state.n1.s
rootState.s // => state.s
}
},
mutations: {
m(state) {
state.s // => state.n1.s
}
},
actions: {
a({state, getters, commit, dispatch}) {
commit("m"); // => mutations["n1/m"]
dispatch("a1"); // => actions["n1/a1"]
getters.g // => getters["n1/g"]
},
a1(){}
}
}
}
}Copy the code
In a module where namespaced=true, the accessed state and getters are from the module’s internal state and getters, and only rootState and rootGetters point to the root module’s state and getters. In addition, in the module, commit triggers mutations within the submodule, while dispatch triggers actions within the submodule. This encapsulation is implemented in VUEX through path matching.
//state
{
"s": "any"
"n1": {
"s": "any",
"n2": {
"s": "any"
}
}
}
// getters
{
"g": function () {},
"n1/g": function () {},
"n1/n2/g": function () {}
}
// mutations
{
"m": function () {},
"n1/m": function () {},
"n1/n2/m": function () {}
}
// actions
{
"a": function () {},
"n1/a": function () {},
"n1/n2/a": function () {}
}Copy the code
In VUEX, we need to construct such a data structure to store each data item, and then rewrite the commit method in the context, and delegate commit(type) to namespaceType to realize the encapsulation of commit method. Similar dispatches are also encapsulated in this way. Getters implements a getterProxy, proxies key to store.getters[namespace+key], and then replaces getters in context with getterProxy. State uses the above data structure. Find the corresponding path state and assign it to context.state, so that all data accessed through the context is inside the module. Now let’s look at the code implementation
installModule (store, state, path, module, hot) { const isRoot = ! path.length; // getNamespace const namespace = store._modules.getnamespace (path); }Copy the code
The construction of all data items, as well as the construction of the context, is in the installModule method of store.js, which first gets the namespace through the path passed in
GetNamespace (path) {let Module = this.root; return path.reduce((namespace, key) => { module = module.getChild(key); return namespace + (module.namespaced ? `${key}/` : ""); }, ""); }Copy the code
The method that gets namespace is an instance method of ModuleCollections, which accesses Modules layer by layer and checks for namespaced properties, If true, the path[index] is spelled on the namespace so that the namespace is complete followed by the implementation of the nested structure state
InstallModule (Store, state, path, module, hot) {// Construct nested state if (! isRoot && ! hot) { const moduleName = path[path.length - 1]; const parentState = getNestedState(state, path.slice(0, -1)); Vue.set(parentState, moduleName, module.state); }}Copy the code
Obtain the parentState corresponding to the state according to the path, where the input parameter state is store.state
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}Copy the code
Where getNestState is used to obtain the corresponding state according to the path. After obtaining parentState, mount module.state on parentState[moduleName]. This creates a nested state structure as described above. After obtaining the namespace, we need to construct the passed getters, mutations, and actions according to the namespace
installModule (store, state, path, module, hot) {
module.forEachMutation((mutation, key) => {
const namespacdType = namespace + key;
this.registerMutation(store, namespacdType, mutation, local);
});
module.forEachAction((action, key) => {
const type = action.root ? type : namespace + key;
const handler = action.handler || action;
this.registerAction(store, type, handler, local);
});
module.forEachGetters((getter, key) => {
const namespacedType = namespace + key
this.registerGetter(store, namespacedType, getter, local);
});
}Copy the code
The construction of getters, mutations and actions has almost the same way, but it is mounted on store._getters,store._mutations,stors._actions respectively. Therefore, we need to analyze the construction process of mutations. The mutations object in the Module is first traversed by forEachMutation, and then registered on the key with namespace+key through ergisterMustions
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)// mutationCopy the code
It’s actually stored on store._mutations[namespace+key]. Now that we have completed half of the encapsulation, we need to implement a context for each module that contains state, getters,commit, and actions. But state,getters can only access state and getters in the Module, commit and actions can only access state and getters in the Module
installModule (store, state, path, module, Hot) {// register the mutation event queue const local = module.context = makeLocalContext(store, namespace, path); }Copy the code
We’ll implement the context in installModule and assign the assembled context to local and module.context, respectively. The local will be passed to getters,mutations, and Actions as parameters in the register
function makeLocalContext (store, namespace, path) { const noNamespace = namespace === ""; const local = { dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); let { type } = args; const { payload, options } = args; if (! options || ! options.root) { type = namespace + type; } store.dispatch(type, payload, options); }, commit: noNamespace ? store.commit : (_type, _payload, _options) => { const args = unifyObjectStyle(_type, _payload, _options); let { type } = args; const { payload, options } = args; if (! options || ! options.root) { type = namespace + type; } store.commit(type, payload, options); }}; return local; }Copy the code
The commit and dispatch methods in context are implemented in the same way. We only analyze the COMMIT, and first determine whether the module is encapsulated by namespace. If so, an anonymous function is returned. A call to store.dispatch will sneak in the incoming type to namespace+type, So the commit[type] we perform on the encapsulated module is actually an event queue that calls store._mutations[namespace+type]
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === "";
Object.defineProperties(local, {
state: {
get: () => getNestedState(store.state, path)
},
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
}
});
return local;
}Copy the code
Then there is state, which is accessed by passing path to getNestedState, which is actually the state in the Module, while getters accesses internal Getters by proxy
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: () => store.getters[type], enumerable: true }) }) return gettersProxy }Copy the code
First declare a proxy object gettersProxy, and then go through store.getters to check whether the path of namespace is fully matched. If so, proxy localType property of gettersProxy to Store. getters[type]. Then return gettersProxy so that localType accessed via local.getters is actually stores. Getters [namespace+type]. Here’s a quick summary of how to get the path’s corresponding namespace (namespaced=true) ->state concatenates to store.state to make it a nested path-based structure -> Register localContext Register localContext
- Dispatch: Namespace -> Flattening parameters -> No root condition triggers the namespace directly +type-> Root or hot condition triggers the type
- Commit -> flat parameter -> No root condition triggers namespace+type-> Root or hot condition triggers namespace type
- State: Searches for State based on path
- Getters: Declares the proxy object, iterates over the Store. Getters object, matches the key and namespace, and points its localType to the full path