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
2
3
4
5
6
7
8
9
10
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}

Like Vue-Router, Vuex also has a static install method, which is defined in src/store.js:

1
2
3
4
5
6
7
8
9
10
11
12
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)
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)
}
}

/**
* Vuex init hook, injected into each instances init hooks list.
*/

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
}
}
}

'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
2
3
4
5
6
7
8
export default new Vuex.Store({
actions,
getters,
state,
mutations,
modules
// ...
})

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
export class Store {
constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
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.`)
}

const {
plugins = [],
strict = false
} = options

// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
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

const state = this._modules.root.state

// 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.forEach(plugin => plugin(this))

if (Vue.config.devtools) {
devtoolPlugin(this)
}
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... },
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

Store.state.a // -> mod uleA
Store.state.b // -> the state of modul eB

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}

get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}

getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}

update (rawRootModule) {
update([], this.root, rawRootModule)
}

register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV ! 'production') {
assertRawModule(path, rawModule)
}

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)
})
}
}

unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return

parent.removeChild(key)
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state

// Store the origin module's state
this.state = (typeof rawState = 'function' ? rawState() : rawState) || {}
}

get namespaced () {
return !!this._rawModule.namespaced
}

addChild (key, module) {
this._children[key] = module
}

removeChild (key) {
delete this._children[key]
}

getChild (key) {
return this._children[key]
}

update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}

forEachChild (fn) {
forEachValue(this._children, fn)
}

forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}

forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}

forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}

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
2
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)

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
2
3
4
5
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}

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
2
3
getChild (key) {
return this._children[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
2
3
addChild (key, module) {
this._children[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
2
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)

Let’s take a look at the definition of’installModule ':

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)
})
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,

//module assets
State: {...},//The state inside the module is already nested, using the'namespaced 'attribute will not affect it
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},

//nested modules
modules: {
//Inherit the namespace of the parent module
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},

//further nested namespaces
posts: {
namespaced: true,

state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})

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
2
3
4
5
6
7
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}

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
2
3
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 + type
if (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)
},

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 + type
if (process.env.NODE_ENV ! 'production' && !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
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})

return local
}

'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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}

‘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
2
3
4
5
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}

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
2
3
4
5
6
7
8
9
10
11
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})

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)
})
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})

function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})


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
)
}
}

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
2
3
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})

Earlier we ignored the “state” initialization logic under non-" root module ", now let’s take a look:

1
2
3
4
5
6
7
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)
})
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function resetStoreVM (store, state, hot) {
const oldVm = store._vm

// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent

// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}

if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}

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
2
3
4
5
6
7
8
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}

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
2
3
4
5
6
store._vm = new Vue({
data: {
$$state: state
},
computed
})

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
2
3
get state () {
return this._vm._data.$$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
2
3
4
5
6
7
8
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})

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
2
3
4
5
6
7
8
9
10
11
if (store.strict) {
enableStrictMode(store)
}

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 })
}

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
2
3
4
5
6
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}

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 '.