Vuex Source Code Analysis
Vuex
In this section, we mainly analyze the initialization process of Vuex, which includes two aspects: installation and Store instantiation process.
The installation process is relatively simple. The following image is a simple mind map about instantiation.
Installation
When we pass’import Vuex from’vuex ‘in the code, we are actually referring to an object, which is defined in’src/index.js’:
1 | export default { |
Like Vue-Router, Vuex also has a static install method, which is defined in src/store.js:
1 | export function install (_Vue) { |
The logic of’install ‘is very simple, assign the passed _Vue to’Vue’ and execute the’applyMixin (Vue) ‘method, which is defined in’src/mixin.js’:
1 | export default function (Vue) { |
'ApplyMixin ‘is the’export default function’, it is also compatible with the Vue 1.0 version, here we only focus on the Vue 2.0 version of the logic, it is actually a global mixed’beforeCreate ‘hook function, its implementation is very simple, that is, the options.store is saved in the’this. $store’ of all components, this options.store is the instance of the’Store ‘object we instantiate, we will introduce later, this is why we can pass the’this. $store’ in the component 'Access to this instance.
Store
After’import Vuex ‘, we will instantiate the’Store’ object, return the’store ‘instance and pass it to the’options’ of’new Vue ', which is the ‘options.store’ we just mentioned.
A simple example is as follows:
1 | export default new Vuex.Store({ |
The constructor function of the Store object receives an object parameter that contains the core concepts of Vuex such as actions, getters, state, mutations, and modules, which are defined in src/store.js:
1 | export class Store { |
We split the instantiation process of’Store ‘into three parts, namely initializing the module, installing the module and initializing the’store._vm’. Next, we will analyze the implementation of these three parts.
Initialization module
Before analyzing module initialization, let’s first understand the significance of modules for Vuex: Due to the use of a single state tree, all the state of the application will be concentrated into a relatively large object, and when the application becomes very complex, the’store ‘object can become quite bloated. To solve the above problem, Vuex allows us to split the’store’ into modules. Each module has its own’state ‘,’ mutation ‘,’ action ‘,’ getter ', and even nested submodules - divided in the same way from top to bottom:
1 | const moduleA = { |
So from the data structure point of view, the design of the module is a tree structure, ‘store’ itself can be understood as a’root module ‘, its following’modules’ is the sub-module, Vuex needs to complete the construction of this tree, the entry of the building process is:
1 | this._modules = new ModuleCollection(options) |
ModuleCollection is defined in src/module/module-collection.js:
1 | export default class ModuleCollection { |
The process of instantiating’ModuleCollection ‘is to execute the’register’ method.
'Register ‘takes 3 parameters, where’path’ represents the path, because our overall goal is to build a module tree, ‘path’ is the path maintained during the construction of the tree; ‘rawModule’ represents the original configuration of the defined module; ‘runtime’ represents whether it is a module created at runtime.
The’register ‘method first creates an instance of’Module’ via’const newModule = new Module (rawModule, runtime) ‘.’ Module ‘is a class used to describe a single module, which is defined in’src/module/module.js’:
1 | export default class Module { |
Take a look at the constructor function of’Module ‘. For each module,’ this._rawModule ‘represents the configuration of the module,’ this._children ‘represents all its submodules, and’this.state’ represents the’state 'defined by this module.
Returning to’register ‘, then after instantiating a’Module’, judging that the length of the current’path ‘is 0, it means that it is a root module, so assign’newModule’ to’this.root ', otherwise you need to establish a parent-child relationship:
1 | const parent = this.get(path.slice(0, -1)) |
Let’s first understand its logic in general: first get the parent module according to the path, and then call the’addChild 'method of the parent module to establish a parent-child relationship.
The last step of’register ‘is to iterate through all’modules’ in the current module definition. According to’key ‘as’path’, recursion calls the’register 'method, so we go back to the logic of establishing the parent-child relationship, and first execute the’this.get (path.slice (0, -1) ’ method:
1 | get (path) { |
The passed’path ‘is the’path’ of its parent module, and then starting from the root module, find the corresponding module layer by layer through the’reduce 'method. During the search process, the’module.getChild (key) ’ method is executed:
1 | getChild (key) { |
In fact, it is the module that returns the corresponding key in the _children of the current module. Then how is the _children of each module added? It is by executing the parent.addChild (path [path.length - 1], newModule) method:
1 | addChild (key, module) { |
So for the next layer of modules of the root module, their parent is the root module, and they will be added to the _children of the root module. Each child module finds its parent module through the path, and then establishes a parent-child relationship through the parent module’s addChild method. Recursion executes this process, ultimately creating a complete module tree.
Installation module
After initializing the module, execute the relevant logic of installing the module. Its goal is to initialize the’state ‘,’ getters’, ‘mutations’, and’actions’ in the module. Its entry code is:
1 | const state = this._modules.root.state |
Let’s take a look at the definition of’installModule ':
1 | function installModule (store, rootState, path, module, hot) { |
The installModule method supports five parameters: store for root store, state for root state, path for access to the module, module for the current module, and hot for Hot Module Replacement.
Next, let’s look at the function logic, which involves the concept of namespaces. By default, the’actions’, ‘mutations’ and’getters’ inside the module are registered in the global namespace - this allows multiple modules to respond to the same’mutation ‘or’action’. If we want a module to have higher encapsulation and reuse, we can make it a namespaced module by adding’namespaced: true '. When a module is registered, all its getters, actions, and mutations are automatically named according to the path registered by the module. For example:
1 | const store = new Vuex.Store({ |
Returning to the installModule method, we first get the namespace from path:
1 | const namespace = store._modules.getNamespace(path) |
The definition of getNamespace is in src/module/module-collection.js:
1 | getNamespace (path) { |
Starting from’root module ‘, through the’reduce’ method to find the sub-module layer by layer, if it is found that the module is configured with’namespaced ‘is true, then put the module’s’key’ into’namesapce ‘, and finally return the complete’namespace’ string.
Back to the’installModule ‘method, save the module corresponding to the’namespace’, so that you can find the module according to the’namespace 'in the future:
1 | if (module.namespaced) { |
The next judgment is not’root module ‘and not’hot’ to perform some logic, we will see later.
Then comes the important logic of constructing a local context environment.
1 | const local = module.context = makeLocalContext(store, namespace, path) |
Take a look at the implementation of’makeLocalContext ':
1 | function makeLocalContext (store, namespace, path) { |
'makeLocalContext ‘supports three parameters related,’ store ‘represents’root store’; ‘namespace’ represents the namespace of the module, and’path ‘represents the’path’ of the module.
This method defines the “local” object. For the “dispatch” and “commit” methods, if there is no “namespace”, they directly point to the “dispatch” and “commit” methods of the “root store”. Otherwise, the method will be created, and the “type” will be automatically concatenated to the “namespace”, and then the corresponding method on the “store” will be executed.
For’getters’, if there is no’namespace ‘, return the’getters’ of’root store 'directly, otherwise return the return value of’makeLocalGetters (store, namespace) ':
1 | function makeLocalGetters (store, namespace) { |
‘makeLocalGetters’ first gets the length of the’namespace ‘, then traverses all the’getters’ under the’root store ‘, and first determines whether its type matches the’namespace’. Only when it matches, we intercept the string behind the’namespace ‘to get the’localType’, and then use the’Object.defineProperty ‘to define the’gettersProxy’. Obtaining the’localType ‘actually accesses the’store.getters [type]’.
Going back to the makeLocalContext method, let’s look at the implementation of state, which is obtained through the getNestedState (store.state, path) method:
1 | function getNestedState (state, path) { |
The logic of’getNestedState ‘is very simple. Starting from’root state’, it searches for the’state ‘of the submodule layer by layer through the’path.reduce’ method, and finally finds the’state 'of the target module.
So after constructing the’local ‘context, we go back to the’installModule’ method, and then it will iterate over the’mutations’, ‘actions’, ‘getters’ defined in the module, performing their registration work respectively, and their registration logic is similar.
registerMutation
1 | module.forEachMutation((mutation, key) => { |
First iterate through the definition of mutations in the module, get each mutation and key, concatenate the key to the namespace, and then execute the registerMutation method. This method actually adds the wrappedMutationHandler method to the _mutations [types] on the root store, and the specific implementation of this method will be mentioned later. Note that _mutations of the same type can correspond to multiple methods.
registerAction
1 | module.forEachAction((action, key) => { |
First, traverse the definition of actions in the module, get each action and key, and judge action.root, if not, concatenate the key to namespace, and then execute the registerAction method. This method is actually to add the wrappedActionHandler method to the _actions [types] on the root store, the specific implementation of which we will mention later. Note that the _actions of the same type can correspond to multiple methods.
registerGetter
1 | module.forEachGetter((getter, key) => { |
First iterate through the definition of getters in the module, get each getter and key, concatenate the key to the namespace, and then execute the registerGetter method. This method actually specifies the wrappedGetter method for the _wrappedGetters [key] on the root store, and the specific implementation of this method will be mentioned later. Note that only one _wrappedGetters of the same type can be defined.
Going back to the installModule method, the last step is to iterate through all the child modules in the module, and recursion executes the installModule method:
1 | module.forEachChild((child, key) => { |
Earlier we ignored the “state” initialization logic under non-" root module ", now let’s take a look:
1 | if (!isRoot && !hot) { |
Earlier we mentioned the’getNestedState ‘method, which starts from the’root state’, and one layer can access the’state ‘corresponding to the’path’ according to the module name, so the establishment of each layer of its relationship is actually through the initialization logic of this’state ‘. The’store._withCommit’ method we will introduce later.
So’installModule ‘actually completes the initialization of’state’, ‘getters’, ‘actions’, and’mutations’ under the module, and completes the installation of all submodules through recursion traversal.
Initialization
The last step in the instantiation of’Store 'is to execute the logic that initializes _vm. Its entry code is:
1 | resetStoreVM(this, state) |
Take a look at the definition of’resetStoreVM ':
1 | function resetStoreVM (store, state, hot) { |
The function of’resetStoreVM ‘is actually to establish the connection between’getters’ and’state ‘, because the acquisition of’getters’ depends on’state ‘by design, and it is expected that its dependencies can be cached, and only when its dependency value changes will it be re-evaluated. Therefore, this is achieved by using the’computed’ calculation property in Vue.
'resetStoreVM ‘first traverses the _wrappedGetters to get the function’fn’ and’key ‘of each’getter’, and then defines’computed [key ] = () => fn (store) '. We mentioned the initialization process of ‘_wrappedGetters’ earlier, where’fn (store) 'is equivalent to executing the following method:
1 | store._wrappedGetters[type] = function wrappedGetter (store) { |
What is returned is the execution function of’rawGetter ‘.’ rawGetter ‘is the user-defined’getter’ function. Its first two parameters are’local state ‘and’local getters’, and the last two parameters are’root state ‘and’root getters’.
Then instantiate a Vue instance _vm and pass in computed:
1 | store._vm = new Vue({ |
We found that the “$$state” attribute is defined in the “data” option, and when we access “store.state”, we will actually access the “get” method of “state” defined on the “Store” class:
1 | get state () { |
It actually accesses’store._vm _data. $$state ‘. So how do’getters’ and’state 'establish dependency logic? Let’s look at this code logic again:
1 | forEachValue(wrappedGetters, (fn, key) => { |
When I access a’getter ‘of’store.getters’ according to’key ‘, I actually access’store._vm [key]’, that is, ‘computed [key]’. When executing the function corresponding to’computed [key] ', the’rawGetter (local.state,…) ’ method will be executed, then the’store.state ‘will be accessed, and then the’store._vm _data. $$state’ will be accessed, thus establishing a dependency relationship. When’store.state ‘changes, it will be recalculated the next time’store.getters’ is accessed.
Let’s take another look at the logic of’strict mode ':
1 | if (store.strict) { |
When in strict mode, _vm will add a wathcer to observe the change of this._data $$state, that is, when the store.state is modified, the store._committing must be true, otherwise it will be warned during development. The default value of store._committing is false, so when will it be true? Store defines the _withCommit instance method:
1 | _withCommit (fn) { |
It wraps an environment around’fn ‘to ensure that’this._committing = true’ when any logic is executed in’fn ‘. So any external modification of’state’ that is not directly operated through the interface provided by Vuex will trigger a warning during development.
Summary
So at this point, the initialization process of Vuex is analyzed. In addition to the installation part, we focus on the instantiation process of the’Store ‘. We want to think of the’store’ as a data warehouse. In order to more conveniently manage the warehouse, we split a large’store ‘into some’modules’, and the whole’modules’ is a tree structure. Each module defines state, getters, mutations, and actions, which are initialized by recursion through the module. In order for the module to have a higher degree of encapsulation and to reuse, the concept of namespace is also defined. Finally, we also define an internal’Vue ‘instance to establish the connection between’state’ and’getters’, and can monitor whether changes to’state ‘come from the outside in strict mode. The only way to ensure that changes to’state’ are made is to explicitly commit’mutation '.