Vuex 原理
Vuex 采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的
- 当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 不能直接改变 store 中的状态
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
# 使用示例
整篇文章都会围绕着这个示例来进行解析,
- moduleA 与 moduleB 分别为两个不同的模块,他们内部有各自的 state、getters、mutations
- 初始示例中,两个模块的 state、getters、mutations 中定义的属性名称都相同
// moduleA
const moduleA = {
state: {
count: 0,
},
getters: {
doubleCount(state) {
return state.count * 2
},
},
mutations: {
increment(state) {
// 这里的 `state` 对象是模块 A 的局部状态
state.count++
},
},
}
// moduleB
const moduleB = {
state: {
count: 0,
},
getters: {
doubleCount(state) {
return state.count * 2
},
},
mutations: {
increment(state) {
// 这里的 `state` 对象是模块 B 的局部状态
state.count++
},
},
}
let store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB,
},
})
// 在组件中访问存储在 store 中的 state
this.$store.a.count // 0
this.$store.a.count // 0
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
接下来就通过上面这个示例,来一步步看 Vuex 的内部实现原理
# 原理解析
首先看下,在组件中为什么可以通过 this.$store
的方式来访问到存储在 store
中的状态?
# Store
实例的注入
Vuex 是通过 Vue 插件的形式被使用的,即Vue.use(Vuex)
,这一点跟 VueRouter 是一样的。而 Vue.use 方法内部会调用插件提供的 install 方法来完成插件的注册,下面就是 Vuex 的 install 方法实现:
let Vue
export function install(_Vue) {
if (Vue && _Vue === Vue) {
// ...
return
}
Vue = _Vue
applyMixin(Vue)
}
function applyMixin(Vue) {
const version = Number(Vue.version.split('.')[0])
// Vue2.x版本
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// ...
}
function vuexInit() {
const options = this.$options
// 1. 根 Vue 实例
if (options.store) {
this.$store =
typeof options.store === 'function' ? options.store() : options.store
} else if (options.parent && options.parent.$store) {
// 2. Vue 子组件实例通过 parent 属性来查找
this.$store = options.parent.$store
}
}
}
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
跟 VueRouter 实例注入方式类似,Vuex Store 实例的注入也是通过 Vue.mixin 方法混入 Vue 的 beforeCreate 这个钩子来实现的,因为 beforeCreate 这个钩子的调用顺序是先父后子,所以子组件中可以通过 parent 属性来访问到根实例上的 $store 属性,这就实现了在任何Vue组件实例中通过 this.$store
的方式来访问到存储在store
中的状态
# Store
的实例化
class Store {
constructor(options = {}) {
// ...
// ======== 1. store 实例属性初始化 ========= //
this._committing = false // 是否正在执行commit的标记
this._actions = Object.create(null) // 存储 actions
this._mutations = Object.create(null) // 存储 mutations
this._wrappedGetters = Object.create(null) // 存储被包装后的 getters
this._modules = new ModuleCollection(options) // 对 module 的处理
this._modulesNamespaceMap = Object.create(null) // 存储具有命名空间的 module 映射
this._makeLocalGettersCache = Object.create(null) // 本地 getters 缓存
// ======== 2. 为commit & dispatch 方法绑定当前store实例作为运行时上下文 ======== //
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)
}
let state = this._modules.root.state
// ======== 3. modules 的注册 ======== //
installModule(this, state, [], this._modules.root)
// ======== 4. store._vm ======== //
resetStoreVM(this, state)
// ======== 5. 调用插件 ======== //
plugins.forEach((plugin) => plugin(this))
}
}
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
整个 Store 构造函数内的逻辑有很多,按照注释内容一步步看。
在1. store 实例属性初始化
中,有一个比较重要的部分,就是对于 modules 的处理,也就是 this._modules = new ModuleCollection(options)
的执行流程
# modules
的初始化
# ModuleCollection
的实例化
ModuleCollection 类的构造函数内部逻辑很简单,直接调用了 register 方法,进行了 modules 的注册工作。这里的 rawRootModule 就是例子中的实例化 Store 时传入的配置对象,
// prettier-ignore
class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule, false)
}
// ====== 注册 modules ====== //
register(path, rawModule, runtime = true) {
// 这里又用到了 Module 类
const newModule = new Module(rawModule, runtime)
if (path.length === 0) { // ====== 1. 注册根 module ====== //
this.root = newModule
} else { // ====== 2. 注册子module ====== //
// 根据path来获取父module
const parent = this.get(path.slice(0, -1))
// 将 子module 添加到 父module 的 _children 属性对象中
parent.addChild(path[path.length - 1], newModule)
}
// 递归注册嵌套的子 modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
// forEachValue 方法
function forEachValue(obj, fn) {
Object.keys(obj).forEach((key) => fn(obj[key], key))
}
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
register 方法的任务也很简单,就是完成 modules 的注册工作。
1、定义在 modules 中的每一个 module 都是 Module
类的实例,包括最顶层的 rawRootModule,且最顶层的 rawRootModule
实例会被赋值给 ModuleCollection
实例的 root
属性
2、这是一个递归的注册过程,如果某个 module 中又定义了 modules ,则会递归的去注册对应的子 module
3、子 module 会被注册到父 module 的 _children
属性中,这是通过 module 实例的 addChild
方法实现的
// @Module addChild 方法
Module.prototype.addChild = function addChild(key, module) {
this._children[key] = module
}
2
3
4
4、父 module 的获取是通过 ModuleCollection 原型上的 get 方法实现的,
// @ModuleCollection get 方法
ModuleCollection.prototype.get = function get(path) {
return path.reduce(function (module, key) {
return module.getChild(key)
}, this.root)
}
2
3
4
5
6
它的内部又调用了 Module 原型上的 getChild 方法
// @Module getChild 方法
Module.prototype.getChild = function getChild(key) {
return this._children[key]
}
2
3
4
一定要记住这里的 module 注册是一个递归的过程,每一层的 module 都是一个 Module 类的实例
经过这里的递归注册之后,上面的例子最终构造出的 store 实例的 _modules
属性的值,差不多就是下面的形式:
{
root: {
_children: {
a: {
_children: {},
_rawModule: {
state: {
count: 0,
},
getters: {
doubleCountA: function doubleCountA(state) {
return state.count * 2;
},
},
mutations: {
increment: function increment(state) {
// 这里的 `state` 对象是模块 A 的局部状态
state.count++;
},
},
},
state: {
count: 0,
},
},
b: {
_children: {},
_rawModule: {
state: {
count: 0,
},
getters: {
doubleCount: function doubleCount(state) {
return state.count * 2;
},
},
mutations: {
increment: function increment(state) {
// 这里的 `state` 对象是模块 B 的局部状态
state.count++;
},
},
},
state: {
count: 0,
},
},
},
_rawModule: {
modules: {
a: {
state: {
count: 0,
},
getters: {
doubleCountA: function doubleCountA(state) {
return state.count * 2;
},
},
mutations: {
increment: function increment(state) {
// 这里的 `state` 对象是模块 A 的局部状态
state.count++;
},
},
},
b: {
state: {
count: 0,
},
getters: {
doubleCount: function doubleCount(state) {
return state.count * 2;
},
},
mutations: {
increment: function increment(state) {
// 这里的 `state` 对象是模块 A 的局部状态
state.count++;
},
},
},
},
},
state: {
},
}
}
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
最顶层称为 root module,然后它有几个重要的属性,这几个属性在各层子孙 module 中都是一样的
_children
: 每个 module 的子 moudle 会存储在_children
中,他们都是 Module 类的实例_rawModule
: 定义在每个 module 中的原始 store 选项state
: 对应 module 的 state
接下来就看下 Module 类的实现
# Module
的实例化
- Module 类构造器内部初始化了一个
_rawModule
属性,用于存储原始的 module 属性值 - 还初始化了一个 state 属性,值为 options.state 的值或函数执行后的结果
- 另外还有一些遍历 getters、actions、mutations 的原型方法,这些方法后面会用到
class Module {
constructor(rawModule, runtime) {
// 用于存储上面提到的子 module
this._children = Object.create(null)
// 存储用户传入的原始 options 对象
this._rawModule = rawModule
const rawState = rawModule.state
// 存储原始 module 的 state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// ...
// 遍历子module
forEachChild(fn) {
forEachValue(this._children, fn)
}
// 遍历getters
forEachGetter(fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
// 遍历actions
forEachAction(fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
// 遍历mutations
forEachMutation(fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
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
看到到这里,Vuex 针对 module 的处理流程就完成了,接下来就看 Vuex 是如何对初始化后的 module 进行注册的
# modules
的注册
这一步主要是通过 installModule
这个方法来实现的。这部分会涉及到两个比较重要的问题:
1、如何做到在不同 module 中定义的 action、mutation 和 getter 方法,接收的 state 参数是对应 module 的局部 state?
2、命名空间 (opens new window)namespaced
属性的设置对于 module 中 actions、mutations、getters 注册的影响
installModule(this, this._modules.root.state, [], this._modules.root)
function installModule(store, rootState, path, module, hot) {
// 如果path是空数组,则判定当前module为根module
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// ...
// 对于非根module的处理
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(function () {
// set state
Vue.set(parentState, moduleName, module.state)
})
}
// 1. 创造一个上下文对象
const local = (module.context = makeLocalContext(store, namespace, path))
// 2. 注册 mutations & actions & getters
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)
})
// 3. 递归处理子 module
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
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
首先来看第一个问题
# 局部 state
拿 getters 为例,它其实可以理解为 state 的计算属性,此外 getters 方法有两个参数,分别为 state 以及其他 getters。
而所谓的局部 state,就是在 moduleA 中定义的 getters,它的 state 参数就是定义在 moduleA 中的局部 state。这是如何做到的呢?
还是以文章开始部分的例子为例,并结合 installModule 方法来看一下这里的处理流程:
- 首先,由于例子中只在 rootModule 中定义了 modules,所以会走到
递归处理子 module
部分
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
2
3
forEachChild
方法在 Module 部分提到过,主要是用来遍历 module 实例的 _children
属性中存储的子 module,也就是例子中的 moduleA 和 moduleB,以 moduleA 为例
此时,传递给 installModule 方法的参数
path.concat(key)
就是['a']
,child
就是 moduleA 的 module 实例
这里正好顺带说一下 namespace 的获取,
const namespace = store._modules.getNamespace(path)
ModuleCollection.prototype.getNamespace = function getNamespace(path) {
var module = this.root
return path.reduce(function (namespace, key) {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
2
3
4
5
6
7
8
9
由于例子中并没有为任何 module 设置 namespaced 属性,所以 module.namespaced 默认为 false,所以最终的 namespace 是一个空字符串。假设这里给 moduleA 设置了namespaced: true
,那最终求得的 namespace 就会成为 a/
。
- 继续向下看,来到
const local = (module.context = makeLocalContext(store, namespace, path))
,因为namespace == ''
,所以下面的代码中省略了 namespace 不为空的处理逻辑
function makeLocalContext(store, namespace, path) {
var noNamespace = namespace === ''
var local = {
dispatch: store.dispatch,
commit: store.commit,
}
Object.defineProperties(local, {
getters: {
get: function () {
return store.getters
},
},
state: {
get: function () {
return getNestedState(store.state, path)
},
},
})
return local
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这个方法最终就是返回了一个 local 对象,这个对象会在后面的注册流程中当做参数传入给各个注册器。这里只需要关注该对象 state 属性的定义,
Object.defineProperties(local, {
// ...
state: {
get: function () {
return getNestedState(store.state, path)
},
},
})
2
3
4
5
6
7
8
可以看到 state 的值,是通过 getNestedState 这个方法获取的,该方法接收两个参数,
store.state
:store 实例的 state 属性path
:对象的属性 path
这个属性的定义是在 Store 类中,
class Store {
// ...
get state() {
return this._vm._data.$$state
}
set state(v) {
if (process.env.NODE_ENV !== 'production') {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
可以看到它实际是对 store._vm._data.$$state
的代理
const state = store._modules.root.state
store._vm = new Vue({
data: {
$$state: state,
},
computed,
})
2
3
4
5
6
7
8
而 getNestedState
这个方法就是实现局部 state 的关键
function getNestedState(state, path) {
return path.reduce((state, key) => state[key], state)
}
2
3
还是以 moduleA 为例,path = ['a'],所以通过 getNestedState 处理后,得到的 state 就是 store.state.a,即 moduleA 的 state
# namespace
关于命名空间的设置,前面也提到了一些,其实这个属性主要影响到的是上面的 const namespace = store._modules.getNamespace(path)
这行代码,非 namespaced 的 module,这个值始终是一个空字符 ''
。
以 moduleA 的 getters 为例,看看设置与不设置这个属性,最终的差异:
- 不设置
- 设置
所以,如果设置了 namespace 为 true,那么当想要访问 moduleA 中的 getters,就只能使用this.$store.getters['a/doubleCountA']
,或者借助辅助函数,如:
...mapActions([
'a/doubleCountA', // -> this['a/doubleCountA']()
])
2
3
这样的方式来访问了,具体可参见文档 (opens new window)
# 注册
还是以 getters 的注册为例,其实整体的过程很简单,主要是将传入的 getters 做一层包裹,并先缓存到了 store 实例的_wrappedGetters
属性中
这里的 rawGetter 就是我们传入的 getter,可以看到它的参数就是在这里被注入的,包括局部 state、局部 getters 以及全局 state 和全局 getters
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
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
那我们实际在应用中访问的 getters 又是怎么定义的呢?
store.getters = {}
var wrappedGetters = store._wrappedGetters
forEachValue(wrappedGetters, function (fn, key) {
Object.defineProperty(store.getters, key, {
get: function () {
return store._vm[key]
},
enumerable: true, // for local getters
})
})
2
3
4
5
6
7
8
9
10
看到这里,有关 store 中各部分组件的注册以及初始化工作,基本上已经完成了。还有一个比较重要的问题,如何做到 store 中的 state 发生变化之后,主动触发视图的更新呢? 其实了解 Vue 双向绑定原理的话,大概也能猜出来需要怎么去做了:那就是将 state 变成响应式的就可以了。下面就看一下这一部分在 Vuex 中是怎么实现的
# 响应式 state
state 的响应式处理,其实也很简单,主要是借助了 Vue 实例的 data 是响应式的这一点
function resetStoreVM(store, state, hot) {
// ...
store._vm = new Vue({
data: {
$$state: state,
},
})
// ...
}
2
3
4
5
6
7
8
9
再结合上面提到的 state 的定义
class Store {
get state() {
return this._vm._data.$$state
}
}
2
3
4
5
# 如何避免用户直接修改 state
主要是利用了 Vue 实例的 $watch 方法,来监听 store._vm._data.$$state
属性的变化,只要第一个函数的返回值发生了变化,就会触发第二个回调函数的执行
function enableStrictMode(store) {
store._vm.$watch(
function () {
return this._data.$$state
},
function () {
if (process.env.NODE_ENV !== 'production') {
assert(
store._committing,
'do not mutate vuex store state outside mutation handlers.'
)
}
},
{ deep: true, sync: true }
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
而在回调函数内部,会有一个对 store._committing
属性的断言,这个值初始为 false,只有在调用_withCommit
来变更 state 的时候,它才会置为 true,且变更成功后又会重置为原始值
Store.prototype._withCommit = function _withCommit(fn) {
var committing = this._committing
this._committing = true
fn()
this._committing = committing
}
2
3
4
5
6
所以原理就是通过深度监听 state 变化 + 判断 _committing
标志位来实现的
# 总结
其实还有其他的一些细节的东西没有说到,不过总体的流程基本上已经过了一遍。ok,就到这里~