阅读分析Vuex源码

不管是Vue框架还是React框架,在实际开发使用的过程中我们都会有很多情况下都会有状态共享的需求,这些状态共享会发生在父子组件和兄弟组件之间,我们为了维护这些状态经常会写很多非必要性的代码,这些代码一多起来,维护就会变得很困难,正是由于有这种需求,人们开发了许多相关的库,从FluxRedux再到Vuex,这些库的大致思路都是:将共享的状态抽离出来,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,达到代码结构化和易维护的目的。

简单状态管理起步

在说到Vuex之前,我们可以先了解下简单的状态管理,也就是Store模式

Store模式

当你有一处需要多个实例共享的状态,可以简单地通过维护一份数据来实现共享:

1
2
3
4
5
6
7
8
9
const sourceOfTruth = {}

const vmA = new Vue({
data: sourceOfTruth
})

const vmB = new Vue({
data: sourceOfTruth
})

现在当 sourceOfTruth 发生变化,vmAvmB都将自动的更新引用它们的视图,但是采用这种方式,在任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。为了解决这个问题,我们可以采用一个简单的Store模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var store = {
debug: true,
state: {
message: 'Hello!'
},
setMessageAction (newValue) {
if (this.debug) console.log('setMessageAction triggered with', newValue)
this.state.message = newValue
},
clearMessageAction () {
if (this.debug) console.log('clearMessageAction triggered')
this.state.message = ''
}
}

所有 store 中 state 的改变,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的 mutation 将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么,此外,每个实例/组件仍然可以拥有和管理自己的私有状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
var vmA = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})

var vmB = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})

接着我们继续延伸约定,组件不允许直接修改属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,这样的话,一个Flux架构就实现了。

目录结构

Vuex 的版本是 3.1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
目录结构

├── helpers.js 提供action、mutations以及getters的查找API
├── index.esm.js
├── index.js 是源码主入口文件,提供store的各module构建安装
├── mixin.js 提供了store在Vue实例上的装载注入
├── module 提供module对象与module对象树的创建功能
│   ├── module-collection.js
│   └── module.js
├── plugins 提供开发辅助插件,如“时光穿梭”功能
│   ├── devtool.js
│   └── logger.js
├── store.js 构建store
└── util.js 提供了工具方法如find、deepCopy、forEachValue以及assert等方法。

一般我看源码都是从入口文件开始看,这里是index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}

入口文件对外暴露了Vuex相关的API,至于为什么这里有一个install,其实是因为Vuex是被当做Vue的插件来使用的,开发一个Vue插件的话就需要对外暴露一个install方法,这个方法第一个参数Vue构造器,第二个参数是一个可选的选项对象。

Store.js

Store.js对外暴露出了Store这个类和install这个方法,在开始分析Store.js之前我们先看一下install

1
2
3
4
5
6
7
8
9
10
11
12
13
export function install (_Vue) {
if (Vue && _Vue === Vue) {
// 报错,已经使用了 Vue.use(Vuex)方法注册了
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}

逻辑很简单,只是把传入的_Vue赋值给Vue,然后调用applyMixin(Vue)方法,这个方法定义在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
34
35
36
37
38
39
40
41
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
// 在全局beforeCreate钩子里面初始化vuex
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) {
// new Vue({
// el: '#app',
// router,
// store,
// render: h => h(App)
// })
// 这里获取的store就是上面初始化Vue的时候传入的store,所以后面我们可以通过this.$store获取到store实例
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 如果是子组件,则从根组件获取store
this.$store = options.parent.$store
}
}
}

applyMixin方法主要是为了在beforeCreate的全局钩子给所有子组件注入$store属性方便后续调用,设置到this上所以后面再全局都可以通过this.$store来访问store对象

Store的实例化

当我们引入Vuex之后下一步的操作就是实例化一个Store对象,会返回一个store实例并且传入Vue的构造器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})

new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

Store的构造函数当中,首先会对执行环境进行断言(是否调用了Vue.use(Vuex)来初始化/是否支持Promise等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.`)
}
// 省略无关代码
}

assert这个方法被定义在utils.js当中:

1
2
3
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}

其实这个方法的作用就是抛出一些异常信息,紧接着定义了一些Store的内部变量:

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
const {
// 一个数组,包含应用在 store 上的插件方法
plugins = [],
// 使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误
strict = false
} = options
// store internal state
// 判断是否是通过mutation更改的state
this._committing = false
// 存放action
this._actions = Object.create(null)
this._actionSubscribers = []
// 存放mutations
this._mutations = Object.create(null)
// 存放getters
this._wrappedGetters = Object.create(null)
// 传入的options对象,其实就是初始化时候传入的对象 new Vuex.Store({options})
// {
// modules: {
// cart,
// products
// },
// strict: debug,
// plugins: debug ? [createLogger()] : []
// })
// 初始化modules
this._modules = new ModuleCollection(options)
// 根据namespace来map对应的module
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
// 用$watch监测store数据的变化
this._watcherVM = new Vue()

这里有个很有意思的知识点,就是我们发现这里创建空对象的时候用的都是Object.create(null),这是因为如果直接用一个{}赋值的话等价于Object.create(Object.prototype),它还会从Object.prototype上继承一些方法如hasOwnPropertyisPrototypeOf等,如果用Object.create(null)则说明这个对象的原型是null也就是没有继承任何对象。
除此之外,在Store的初始化过程中还有几个主要的方法,下面进行逐一的分析:

模块的初始化

由于Store使用的是单一的状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割:

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 // -> moduleA 的状态
store.state.b // -> moduleB 的状态

完成这种树形结构的构建入口就是:

1
2
3
4
5
6
7
8
9
10
// 传入的options对象,其实就是初始化时候传入的对象 new Vuex.Store({options})
// {
// modules: {
// cart,
// products
// },
// strict: debug,
// plugins: debug ? [createLogger()] : []
// })
this._modules = new ModuleCollection(options)

ModuleCollection这个类的定义在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
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import Module from './module'
import { assert, forEachValue } from '../util'

export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}

// 获取对应于path的Module
get (path) {
// this.root根Module
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') {
// 检测module对应的函数的形式是否正确
assertRawModule(path, rawModule)
}
// 构建Module对象
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
// 获取Parent Module
const parent = this.get(path.slice(0, -1))
// 添加子Module
parent.addChild(path[path.length - 1], newModule)
}

// register nested modules
if (rawModule.modules) {
// 递归注册子Module
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)
}
}

function update (path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}

// update target module
targetModule.update(newModule)

// update nested modules
if (newModule.modules) {
for (const key in newModule.modules) {
if (!targetModule.getChild(key)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
}

const functionAssert = {
assert: value => typeof value === 'function',
expected: 'function'
}

const objectAssert = {
assert: value => typeof value === 'function' ||
(typeof value === 'object' && typeof value.handler === 'function'),
expected: 'function or object with "handler" function'
}

// actions使用objectAssert是因为在带命名空间的模块注册全局action时action的定义会放在函数handler中
const assertTypes = {
getters: functionAssert,
mutations: functionAssert,
actions: objectAssert
}

function assertRawModule (path, rawModule) {
Object.keys(assertTypes).forEach(key => {
if (!rawModule[key]) return
const assertOptions = assertTypes[key]
// 循环getters/mutations/actions
// value和type对应函数体和函数名
forEachValue(rawModule[key], (value, type) => {
assert(
assertOptions.assert(value),
makeAssertionMessage(path, key, type, value, assertOptions.expected)
)
})
})
}

function makeAssertionMessage (path, key, type, value, expected) {
let buf = `${key} should be ${expected} but "${key}.${type}"`
if (path.length > 0) {
buf += ` in module "${path.join('.')}"`
}
buf += ` is ${JSON.stringify(value)}.`
return buf
}

实例化ModuleCollection其实就是执行register方法,这个方法接受3个参数,其中path参数就是module的路径,这个值是我们拆分module时候modulekey组成的一个数组,以上面为例的话,moduleAmoduleBpath分别为["a"]["b"],如果他们还有子module则子modulepath的形式大致如["a","a1"]/["b","b1"],第二个参数其实是定义module的配置,像rawRootModule就是我们构建一个Store的时候传入的那个对象,第三个参数runtime表示是否是一个运行时创建的module,紧接着在register方法内部通过assertRawModule方法遍历module内部的gettersmutationsactions是否符合要求,紧接着通过const newModule = new Module(rawModule, runtime)构建一个module对象,看一眼module类的实现:

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
65
66
67
68
69
70
71
import { forEachValue } from '../util'

// Base data struct for store's module, package with some attribute and method
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
// state() {
// return {
// // state here instead
// }
// }
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)
}
}
}

只是简单地描述了构建出来的每个模块的一些属性和方法,回到上面的register函数,构建完Module之后,我们先判断path的长度,如果长度为0说明是根module,将它赋值给this.root,否则的话获取到这个moduleparent,然后通过moduleaddChild方法建立模块之间的父子关系:

1
2
3
4
// 获取Parent Module
const parent = this.get(path.slice(0, -1))
// 添加子Module
parent.addChild(path[path.length - 1], newModule)

这里调用了get方法,传入的pathparent模块的path

1
2
3
4
5
6
get (path) {
// this.root根Module
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}

因为path是整个模块树的路径,这里通过reduce方法一层层解析去找到对应模块,查找的过程是用的module.getChild(key)方法,返回的是this._children[key],这些_children就是通过执行parent.addChild(path[path.length - 1], newModule)方法添加的,就这样,每一个模块都通过path去寻找到parent`module,然后通过addChild建立父子关系,逐级递进,构建完成整个module`树。

模块的安装

接下来回到Store.js,初始化modules之后会执行一些bind操作:

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

这是为了当我们在组件内部使用this.$store.commit/this.$store.dispatch方法时候的this指向的是当前的store而不是组件本身,这里先略过commitdispatch方法的实现,先分析Store.js的初始化操作,在初始化module之后会进行module安装的一些操作:

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

看一眼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
41
42
function installModule (store, rootState, path, module, hot) {
// 判断是否是根module
const isRoot = !path.length
// 获取module的命名空间
const namespace = store._modules.getNamespace(path)

// 将module的命名空间和module本身一一对应存储在_modulesNamespaceMap对象里面方便后续查找
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)
})
}

这个方法接受5个参数:store意味着root storestate就是root statepath是当前模块路径、module是当前模块,hot表示是否是热更新。
首先用一个变量isRoot判断是否是根module,然后通过ModuleCollection类的getNamespace获取当前路径的命名空间,这里提一下Vuex里面命名空间的概念:

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
例如:

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: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},

// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},

// 进一步嵌套命名空间
posts: {
namespaced: true,

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

回到installModule方法,我么可以看一下根据path来获取namespace的方法实现:

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 + '/' : '')
}, '')
}

从根module开始,通过reduce方法沿着path一层层查找子module,然后如果发现该module配置了namespaced,就把该path拼接到namespace后面,最后返回完整的路径。
接下来如果该module配置了namespaced,则把该modulenamespacemodule本身一一对应存储到_modulesNamespaceMap对象里面方便后续查找:

1
2
3
4
// 将module的命名空间和module本身一一对应存储在_modulesNamespaceMap对象里面方便后续查找
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}

紧接着是非root module下的模块state初始化逻辑:

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

先通过getNestedState获取父模块的state,这个方法的实现大同小异,都是通过reduce函数一层层查找到子模块的state

1
2
3
4
5
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}

随后我们拿到子module的名称,调用store对象的_withCommit方法,这个方法里面的函数执行的操作是给父模块的state添加一个名字是module的名称的响应式属性,看一下这个方法的作用:

1
2
3
4
5
6
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}

_committing的初始值为false,用来判断是否是通过mutation来更改的state,因为在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误,在Store.js的源码中有相关代码体现这一点:

1
2
3
4
5
6
7
8
9
10
11
12

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

store._vm是一个在Store.js内置的Vue对象:

1
2
3
4
5
6
store._vm = new Vue({
data: {
$$state: state
},
computed
})

然后通过Vue的实例方法$watch监听每一次state的变化,通过断言判断当前state的变化是否是通过提交一个mutation来引起的,不是的话就报错do not mutate vuex store state outside mutation handlers

初始化非root module下的state之后下一步操作是构造一个当前module的上下文环境:

1
const local = module.context = makeLocalContext(store, namespace, path)

makeLocalContext支持3个参数,分别是root store、当前modulenamespace以及当前modulepath

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
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
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
}

通过一个变量noNamespace判断module是否配置了namespaced属性,然后构建一个local对象,这个对象包含了commitdispatch方法,两个方法定义过程差不多,以commit为例,如果没有namespaced属性,这个commit直接指向了store.commit,否则构建一个函数,这个函数首先会对传入的参数顺序进行格式化,unifyObjectStyle方法兼容了载荷和对象风格的两种提交方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}

if (process.env.NODE_ENV !== 'production') {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}

return { type, payload, options }
}

如果是对象风格的提交方式则对参数位置进行调整,然后返回一个调整位置后的对象,如果没有在commit方法里面设置root:true参数,则将typenamespace拼接之后执行commit方法,如果设置了root:true意味着允许在命名空间模块里提交根的mutation

构建完local对象后会在local对象上定义两个属性gettersstate

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

state的获取比较简单,就是根据root state和当前modulepath获取该modulestate,我们看getters的实现,如果没有namespace,直接返回root storegetters,否则调用makeLocalGetters获取对应namespacegetters,看一下makeLocalGetters方法的实现:

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
}

这个方法对this.getters上所有的可玫举属性进行遍历,然后截取type的包含namespace的部分和传入的namespace进行对比,找到后截取type里面的后半部分,也就是不包含namespace的部分,然后定义了gettersProxyget属性并将其返回。

回到installModule方法,在完成构建local之后,会循环遍历module中定义的mutationactiongetters然后执行注册逻辑,这几个操作都差不多,我们看一个mutation的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})

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

export function forEachValue (obj, fn) {
// 将对象里面的每一项组合成数组
Object.keys(obj).forEach(key => fn(obj[key], key))
}

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

实际上就是给root store_mutations对象的对应type`push一个处理函数,这个函数调用时候会将handlerthis指向root store。 在installModule的最后会循环遍历子module然后执行子moduleinstallModule`方法:

1
2
3
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})

至此,installModule方法分析完毕

初始化store vm

这是实例化Store的最后一步,通过resetStoreVM方法初始化vm以及注册_wrappedGetters,其中有些代码上面分析过,这里先贴出全部相关代码:

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

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。这里首先遍历wrappedGetters得到对应的fn组成的数组,然后将其定义为一个个计算属性computed[key] = () => fn(store)fn(store)其实就是执行了下面的方法:

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

_随后给store.getters新增属性,访问这些属性也就是使用this.store.getters的时候拿到的是key对应的计算属性的值:

1
2
3
4
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})

执行这个getter对应的函数等价于执行了computed[key] = () => fn(store)这个计算属性对应的函数,由于这个函数依赖了store,所以根据计算属性的特性在store变化的时候这个getter也会得到相应的更新。
方法的最后,处理了hotUpdate时候的逻辑,即销毁旧的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hotUpdate (newOptions) {
this._modules.update(newOptions)
resetStore(this, true)
}

function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// ...
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())
}
}

至此,整个Store.js的主题初始化流程已经分析完毕,下面分析一写内置函数以及辅助函数的实现

工具函数

更改Vuexstore中的状态的唯一方法是提交mutationVuex中的mutation非常类似于事件:每个mutation都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

1
2
3
4
5
6
7
8
9
10
11
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

我们不能直接调用一个mutation handler,需要以相应的type调用store.commit方法:

1
store.commit('increment')

看一下commit方法相关的实现:

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
commit (_type, _payload, _options) {
// 将对象风格的commit格式化
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)

const mutation = { type, payload }
// 通过mutation type找到对应的回调函数handler并执行
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 通知所有的订阅者
this._subscribers.forEach(sub => sub(mutation, this.state))

if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}

首先同样是将载荷方式和对象方式的commit格式化,然后找到type对应的mutation,在确保数据更新方式正确的情况下循环执行mutation里面的方法,然后所有mutation相关的订阅者。

dispatch的逻辑要稍微复杂一些,因为通过dispatch分发action是可以执行异步操作的,然后在action内部执行异步操作后再commit一个mutation

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
dispatch (_type, _payload) {
// 对象风格的dispatch格式化
const {
type,
payload
} = unifyObjectStyle(_type, _payload)

const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 从 3.1.0 起,subscribeAction 也可以指定订阅处理函数的被调用时机应该在一个 action 分发之前还是之后 (默认行为是之前)
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}

const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)

return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
}

同样是先格式化参数,然后type对应的action,先判断该type是否存在,然后执行_actionSubscribers里面在action分发之前的回调,这是3.1.0开始有的新功能,可以指定订阅处理函数的被调用时机应该在一个 action 分发之前还是之后 (默认行为是之前),这个功能多用于插件使用,然后分发action,如果同一个typeaction有多个就用Promise.all去分发,否则直接传入载荷,在执行分发action完毕之后执行_actionSubscribers里面在action分发之后的回调,至此完成一次dispatch

辅助函数

为了解决重复代码的冗余性,Vuex对外提供了一些工具函数,这些工具函数会自动帮我们生成计算属性减少工作量,这些函数都位于src/helpers.js目录下,这里可以分析一下mapState的实现:

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
/**
* Reduce the code which written in Vue.js for getting the state.
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
* @param {Object}
*/
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})

mapState函数可以传递两个参数,第一个参数是可选参数,代表命名空间字符串,对象形式的第二个参数的成员可以是一个函数:

1
mapState(namespace?: string, map: Array<string> | Object<string | function>): Object

执行mapState其实就是执行normalizeNamespace返回的函数,这个函数的作用也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/**
* Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
* @param {Function} fn
* @return {Function}
*/
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}

判断是否传入了namespace参数,没有的话,将namespace赋值给map参数,如果传了namespace则拼接好路径,最后将处理好的map作为states传入处理函数。这个函数首先会对states执行normalizeMap处理,这个方法的作用是把我们传入mapState的数组/对象统一转换成一个内部元素都是形如{ key: 'a', val: 1 }的数组:

1
2
3
4
5
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}

然后循环处理数组,结构每一项的keyval,用一个空对象存储,key作为这个空对象reskeykey对应的值是一个名为mappedState的函数,在函数内部获取到了stategetters,然后再判断数组的val是否是一个函数,是的话直接调用,并传入stategetters,否则直接返回state[val]。最后将构建好的res对象返回,通过对象展开运算符将这个res填充到computed中。

总结

Vuex的模式下,我们的组件树构成了一个巨大的视图,无论该组件在树的哪个位置,它都可以获取状态或者触发状态更新的行为,通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护:

Vuex

文章作者: 李牧羊
文章链接: https://www.limuyang.cc/2019/03/29/阅读分析Vuex源码/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Atlantis