Store实例化
Store
类是Vuex的核心,它负责状态的管理、事件的派发与监听等工作。Store
类定义在store.js
文件中,如下:
// store.js
export class Store {
constructor(options = {}) {
// ...
}
//...
}
2
3
4
5
6
7
下面我们将结合例子,一步步的揭开Store
类的面纱。
# 1. 构造函数
Store
类的构造函数接收一个参数options
,它是一个对象,包含以下属性:
state
:状态对象,默认为空对象actions
:action对象,默认为空对象mutations
:mutations对象,默认为空对象getters
:getters对象,默认为空对象modules
:模块对象,默认为空对象plugins
:插件列表,默认为空数组strict
:是否为严格模式,默认为false
Store
类构造函数的代码如下:
点击查看代码
constructor(options = {}) {
// 自动安装Vuex,确保在使用script脚本引入vuex时的自动安装
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.`)
// 不支持Promise提示需要提供polyfill,因为vuex依赖Promise(actions)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
// 只能使用new操作符调用Store
assert(this instanceof Store, `Store must be called with the new operator.`)
}
const {
plugins = [], // 插件列表
strict = false // 是否为严格模式
} = options
// 获取传入的state,如果为函数,则执行函数并返回结果
// 否则直接返回state,为空时默认值是空对象
let {
state = {}
} = options
if (typeof state === 'function') {
state = state() || {}
}
// 各种内部状态标识
this._committing = false // 是否正在提交,用于mutation标记
this._actions = Object.create(null) // 存放action是对象
this._actionSubscribers = [] // 存放action订阅者
this._mutations = Object.create(null) // 存放mutations对象
this._wrappedGetters = Object.create(null) // 存放getters,用于计算属性
this._modules = new ModuleCollection(options) // 创建根模块树
this._modulesNamespaceMap = Object.create(null) // 模块命名控件映射
this._subscribers = [] // 订阅列表
this._watcherVM = new Vue() // 一个vue实例,用于触发watcher
// bind commit and dispatch to self
// 定义dispatch和commit为绑定函数, 将其this属性绑定至当前的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)
}
// strict mode
// 标记当前实例是否为严格模式
this.strict = strict
// 安装根模块
// 1. 注册state、actions、mutations, getters
// 2. 在_modulesNamespaceMap收集模块
// 3. 递归安装子模块
installModule(this, state, [], this._modules.root)
// 将getters和state对象加入响应式系统
// 将__wrappedGetters对象注册成计算属性,使用计算属性来实现其惰性求值的机制
resetStoreVM(this, state)
// apply plugins
// 安装插件
plugins.forEach(plugin => plugin(this))
// 安装devtools插件
if (Vue.config.devtools) {
devtoolPlugin(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
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
# 1.1. 自动安装
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
2
3
当window
存在时说明在浏览器的宿主环境中。当使用script
标签引入Vuex和Vue时,会自动执行install
方法。这个script
为引入的使用提供了便利。#731 (opens new window)
# 1.2. 非生产环境下的断言
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.`)
}
2
3
4
5
- 如果未安装Vuex就使用
new Store
,则提示需要安装才可以使用 - 如果宿主环境不支持
Promise
,则提示需要提供一个Promise
的polyfill,这是因为Vuex的actions
处理依赖Promise
。 - 如果没有使用
new
操作符调用Store
,则提示需要使用new
操作符。`
# 1.3. 初始化各种实例属性
const {
plugins = [], // 插件列表
strict = false // 是否为严格模式
} = options
// ...
this._committing = false // 是否正在提交,用于mutation标记
this._actions = Object.create(null) // 存放action的对象
this._actionSubscribers = [] // 存放action订阅者
this._mutations = Object.create(null) // 存放mutations对象
this._wrappedGetters = Object.create(null) // 存放getters计算属性
this._modules = new ModuleCollection(options) // 创建根模块树
this._modulesNamespaceMap = Object.create(null) // 模块与命名空间映射
this._subscribers = [] // 订阅列表
this._watcherVM = new Vue() // 一个vue实例,用于触发watcher
// ...
this.strict = strict
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_committing
:是否正在提交,默认值为false
,当该值被标记为true
时,则state
允许被修改,参考下文的this._withCommit
方法。_actions
:存放所有action
的对象,默认值为空对象。_actionSubscribers
:存放action
订阅者,默认值为空数组,在使用dispatch
派发action
时,会遍历并触发订阅者,具体查看后文的dispatch
方法。_mutations
:存放所有mutation
的对象,默认值为空对象。_wrappedGetters
:存放所有getters
的包装函数的对象,默认值为空对象。_modules
:根模块树,是Vuex
模块化支持的基础,使用ModuleCollection
类实例化,由上一篇《ModuleCollection与Module类》可知,其最终为一个具有一个root
属性的模块树,后文的installModule
方法会使用该模块树安装模块。_modulesNamespaceMap
:模块命名空间映射,定义的Vuex模块都会被映射到该对象的属性中,辅助函数的模块会从该对象获取。_subscribers
:mutation
订阅列表,默认值为空数组,在使用commit
提交mutation
时,会遍历该数组并执行订阅者,具体查看下文的commit
方法_watcherVM
:一个Vue
实例,用于触发watcher
,是store.watch
API实现的基础,详见后文的watch
实例方法strict
:是否开启严格模式,从options
中获取,默认值为false
。在严格模式下,任何在mutation处理函数以外修改Vuex state的操作都会抛出错误。
# 1.4. 增加绑定方法
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)
}
2
3
4
5
6
7
8
此处在Store实例增加了dispatch
和commit
两个绑定方法,这两个方法都是绑定到store
实例上的。我们知道,dispatch
和commit
是可以在action中解构使用的,使用绑定函数,可以让它们的this
始终指向store
实例。如下:
new Vuex.Store({
mutations: {
INCREMENT (state) {
state.count++
}
}
actions: {
increment ({ commit }) {
commit("INCREMENT")
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
以上是Vuex在实例化时安装模块和响应式处理之前初始化的属性和方法,目前仅说明这些属性的基本作用,详细的内容需要依赖后文的解析。
# 1.5. 安装根模块
installModule(this, state, [], this._modules.root)
安装根模块调用了installModule
函数,该函数接收当前store实例、state
和根模块this._modules.root
作为参数,其中:
(1)state
由以下代码定义:
let {
state = {}
} = options
if (typeof state === 'function') {
state = state() || {}
}
2
3
4
5
6
即options.state
可以是一个函数,如果函数的返回值不是falsy
的函数则会将其结果作为state
。state
的默认值是一个空对象。
注意
Vuex并未对options.state
的类型做任何处理,所以state
可以是非falsy
任何类型。但是无论在何种情况下,你应该保证state
是一个对象。这是为了保证Vuex
的响应式和模块系统的处理正确。
(2)this._modules.root
由以下代码定义:
this._modules = new ModuleCollection(options) // 创建根模块树
由上一篇文章《ModuleCollection与Module类》可知,this._modules
最终是一个具有一个root
属性的模块树,root的结构和说明如下:
{
runtime: false, // 用于模块是标记静态注册还是动态注册的
state: { // 原始的options.state的获取结果
count: 0
},
_children: {}, // 子模块
_rawModule: {}, // 传入的原始options配置对象
get namespaced: () => {}, // 是否开启了命名空间
addChild: (key, module) => {}, // 添加子模块
removeChild: (key) => {}, // 删除子模块
getChild: (key) => {}, // 获取子模块
update: (rawModule) => {}, // 更新模块
forEachChild: (fn) => {}, // 遍历子模块_children并执行指定的操作
forEachGetter: (fn) => {}, // 遍历_rawModule.getters并执行指定的操作
forEachAction: (fn) => {}, // 遍历_rawModule.actions并执行指定的操作
forEachMutation: (fn) => {} // 遍历_rawModule.mutations并执行指定的操作
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行installModule(this, state, [], this._modules.root)
安装根模块时,会对this._modules.root
的state
、getters
、mutations
、actions
进行处理,同时也会递归安装子模块,并与_modulesNamespaceMap
做映射。关于根模块安装的细节,我们将在下文的installModule
函数中讲解。
# 1.6. 响应式处理
resetStoreVM(this, state)
响应式处理主要是在store实例上添加_vm
属性,该属性是一个Vue
实例,并将state
作为Vue
实例的data
属性,从而实现响应式。同时,也会对getters
做处理,具体见下文的resetStoreVM
函数的讲解。
# 1.7. 安装插件
const {
plugins = [], // 插件列表
strict = false
} = options
plugins.forEach(plugin => plugin(this))
// 安装devtools插件
if (Vue.config.devtools) {
devtoolPlugin(this)
}
2
3
4
5
6
7
8
9
10
11
12
Store
实例化时可以传入插件列表,每一个插件都是一个函数,会接收当前的store实例作为参数。一般我们会使用插件来订阅状态的变化,比如在Vuex
中,我们可以使用vuex-persistedstate
插件来将state
持久化到localStorage
中,也可以使用vuex-router-sync
插件来将router
和store
进行同步。在默认情况下,如果浏览器安装了vue-devtools
插件,Vuex
会自动安装devtools
插件,该插件会向devtools
发送状态变化和action
的日志,方便我们调试。
以下是一个简单的日志打印插件的示例:
function logger(store) {
let prevState = JSON.stringify(store.state)
store.subscribe((mutation, state) => {
console.log(mutation.type)
console.log('prev state: ' + prevState)
console.log('next state: ' + JSON.stringify(state))
})
}
2
3
4
5
6
7
8
以上就是Store
类初始化的过程,初始化后,store
会是一个结构上如下的对象:
{
commit: function boundCommit(type, payload, options) {}, // commit绑定函数
dispatch: function boundDispatch(type, payload) {}, // dispatch绑定函数
strict: false, // 严格模式
get state() {}, // state代理,待讲解
getters: {}, // getters代理,待讲解
_wrappedGetters: {}, // 被包装后的getters,待讲解
_actions: {} // actions存储
_actionSubscribers: [], // action订阅列表
_mutations: {}, // mutations存储
_subscribers: [], // mutation订阅列表
_modules: ModuleCollection {root: Module {}}, // 模块树
_modulesNamespaceMap: {}, // 模块命名空间映射
_vm: Vue {}, // Vue实例,用于响应式处理
_watcherVM: Vue {}, // Vue实例,用于触发watcher
_committing: false, // 是否正在commit
[[Prototype]]: Object // 原型上的一些实例方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
接下来我们来解析installModule
函数和resetStoreVM
函数。
# 2. installModule函数
函数定义:installModule(store, rootState, path, module, hot)
其接收5个参数的说明如下:
store
:当前store
实例rootState
:根state
path
:当前模块的路径,数组形式,如['a', 'b']
module
:当前模块的配置对象,即要安装的模块对象,里面含state
,getters
,mutations
,actions
等属性hot
:是否是热更新
函数的代码如下:
点击查看代码
function installModule(store, rootState, path, module, hot) {
// 判断当前模块是否为根模块
const isRoot = !path.length
// 获取当前模块的命名空间
const namespace = store._modules.getNamespace(path)
// 带命名空间的模块在命名空间映射中登记模块
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// 设置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)
// 注册模块的mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key // 拼接命名空间
registerMutation(store, namespacedType, mutation, local)
})
// 注册模块的action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key // 转化key
const handler = action.handler || action // 获取action的handler
registerAction(store, type, handler, local)
})
// 注册模块的getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 遍历module._children,拼接命名空间,递归安装模块的子模块
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
43
44
45
46
47
48
代码解析:
# 2.1 模块映射
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
2
3
4
5
首先,根据path
的长度标识当前模块是否为根模块,在Store
构造函数中调用installModule
时,path
为空数组,所以isRoot
为true
,即代表安装根模块;接着令namespace
为当前按安装模块的命名空间,getNamespace
方法就是将['a', 'b']
这样的path
路径转为一个以/
分隔的字符串,如a/b/
,安装根模块时,path
为[]
,所以namespace
为空字符串;最后如果安装模块的namespaced
属性为true
,则会以namespace
的值为键,将当前模块映射到_modulesNamespaceMap
。换句话说,只有当模块的namespaced
属性为true
时,才会将当前模块映射到_modulesNamespaceMap
中,否则其对应的actions
、mutations
都被注册到全局模块中,如下例子(后文会讲解注册过程):
new Vuex.Store({
namespaced: true,
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
modules: {
cart: {
namespaced: true,
list: [],
mutations: {
add(state, payload) {
state.list.push(payload)
}
}
},
goods: {
name: "apple",
mutations: {
add(state, payload) {
state.name = payload
}
}
}
}
})
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
此时_modulesNamespaceMap
和_mutations
如下:
# 2.2 设置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)
})
}
2
3
4
5
6
7
在非根模块的非热更新模式下,会设置当前模块的state
:
- 获取要安装的模块的父模块,通过
getNestedState
函数来获取 - 往父模块里设置当前模块的
state
,通过Vue.set
函数来设置,因为state
的修改只能通过提交mutation
来修改,所以这里通过_withCommit
函数来提交一个mutation
。
实例化Store时,安装的是根模块,所以不会执行以上逻辑。
getNestedState
函数的函数定义如下,其功能是从根state开始,按照给定的路径找到对应的嵌套的state:
function getNestedState(state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
2
3
4
5
经过这一步的处理,我们不难知道,实例化Store时,如果传入的是一个包含模块的配置对象,那么state
最终会是一个包含所有模块的state嵌套对象,如下:
const store = new Vuex.Store({
state: {
count: 0
},
modules: {
a: {
state: {
name: "Foo"
}
},
b: {
state: {
name: "Bar",
},
modules: {
c: {
state: {
name: "Baz"
}
}
}
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 2.3 注册mutation
、action
、getter
const local = module.context = makeLocalContext(store, namespace, path)
// 遍历module._rawModule.mutations,注册模块的mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key // 拼接命名空间
registerMutation(store, namespacedType, mutation, local)
})
// 遍历module._rawModule.actions,注册模块的action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key // 转化key
const handler = action.handler || action // 获取action的handler
registerAction(store, type, handler, local)
})
// 遍历module._rawModule.getters,注册模块的getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
首先通过makeLocalContext
函数,创建一个局部上下文对象,用于访问局部状态和命名空间,并将其赋值给local
和module.context
,makeLocalContext
函数的定义如下:
function makeLocalContext(store, namespace, path) {
const noNamespace = namespace === '' // 没有命名空间
// 局部的包含dispatch、commit、getters和state的对象
const local = {
// 如果没有命名空间则使用store的dispatch
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
// 统一不同调用风格的参数
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
//
if (!options || !options.root) { // 不是根调用(dispatch("xxx", payload, {root: true}))
type = namespace + type // 给type拼上命名空间
// 未注册对应的action则提示错误
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
// 调用store的dispatch
return store.dispatch(type, payload)
},
// 解析同dispatch
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {// 不是根调用(dispatch("xxx", payload, {root: true}))
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
// 给local添加getters和state延迟获取的方法
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters // 无命名空间返回store的getters
: () => makeLocalGetters(store, namespace)
},
state: {
// 根据路径获取嵌套的state
get: () => 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
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
makeLocalContext
接收三个参数,分别是当前的Store实例store
、命名空间namespace
、路径path
。其核心的逻辑是创建一个local
对象并返回,该对象包含dispatch
、commit
、getters
和state
四个属性,这四个属性都会根据当前模块是否有命令空间来进行不同的处理。
dispatch
:如果当前模块没有命名空间时,则直接使用store.dispatch
,否则返回一个新的函数:- 统一不同调用风格的参数,通过调用
unifyObjectStyle
函数统一转为一个含{type, options,payload }
属性的对象 - 如果不是根调用(
options.root
为false
),则给type
拼上命名空间 - 判断是否存在对应的
action
,如果不存在则提示错误 - 调用
store.dispatch(type, payload)
,此时的type
可能是一个带了命名空间的字符串,这样就能找到对应的action
- 统一不同调用风格的参数,通过调用
commit
:和dispatch
类似,只是最终调用的是store.commit
state
:state
被定义成了getter函数,这是因为state
在后续的更新中可能会发生变化,所以需要延迟获取。如果当前模块没有命名空间,则直接返回store.state
,否则通过getNestedState
函数获取嵌套的state,并返回getters
:getters
同样被定义成了getter函数,原因与state
一致。如果当前模块没有命名空间,则直接返回store.getters
,否则通过makeLocalGetters
函数获取嵌套的getters并返回,makeLocalGetters
函数的定义如下:
function makeLocalGetters(store, namespace) {
const gettersProxy = {} // 定义一个getters代理对象
// 获取命名空间长度
const splitPos = namespace.length
// 遍历全局的getters
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
// 检查当前getter是否匹配命名空间(以该命名空间开头)
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
}
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
makeLocalGetters
的核心逻辑就是创建一个代理对象gettersProxy
,然后遍历store.getters
的所有计算属性,根据命名空间进行过滤,将匹配的getter
添加到gettersProxy
中,并定义为getter属性,这样就实现了局部的getter代理访问。如下例子,local.getters.fullName
最终会被代理至store.getters["user/fullName"]
。
new Vuex.Store({
state: {
a: 1,
b: 2
},
getters: {
total: state => state.a + state.b
},
modules: {
user: {
state: {
lastName: 'Foo',
firstName: 'Bar'
},
getters: {
fullName: state => state.lastName + ' ' + state.firstName
}
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
回到local
定义所在处,在定义完local
这个局部的上下文后会分别遍历module._rawModule
的mutations
、actions
、getters
属性进行注册。上文中我们已经提到过,在调用new Store()
时构造函数中会执行以下代码给store实例增加一个含有root
属性的_modules
属性:
this._modules = new ModuleCollection(options) // 创建根模块树
this._modules.root
,其结构如下:
{
{
runtime: false, // 用于模块是标记静态注册还是动态注册的
state: { // 原始的options.state的获取结果
count: 0
},
_children: {}, // 子模块
_rawModule: {}, // 传入的原始options配置对象
get namespaced: () => {}, // 是否开启了命名空间
addChild: (key, module) => {}, // 添加子模块
removeChild: (key) => {}, // 删除子模块
getChild: (key) => {}, // 获取子模块
update: (rawModule) => {}, // 更新模块
forEachChild: (fn) => {}, // 遍历子模块_children并执行指定的操作
forEachGetter: (fn) => {}, // 遍历_rawModule.getters并执行指定的操作
forEachAction: (fn) => {}, // 遍历_rawModule.actions并执行指定的操作
forEachMutation: (fn) => {} // 遍历_rawModule.mutations并执行指定的操作
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(1)首先是遍历遍历_rawModule.mutations
注册Mutation:
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key // 拼接命名空间
registerMutation(store, namespacedType, mutation, local)
})
2
3
4
registerMutation
函数定义如下:
/**
* 注册mutation
* @param {Store} store store实例
* @param {string} type mutation的type,可以是key,也可以是命名空间+key
* @param {Function} handler 处理函数
* @param {Object} local 局部上下文
*/
function registerMutation(store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = []) // 初始化
// 添加处理函数
entry.push(function wrappedMutationHandler(payload) {
// 以store作为this,局部上下文的state和外部传入的payload作为参数调用处理函数
handler.call(store, local.state, payload)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
遍历mutations
后处理的过程是先通过命名空间与当前·mutation
的名称计算要注册的mutation
的type
。然后将其处理函数包装后添加到store._mutations[type]
中。包装后的函数会接收当前store
、局部作用域下的state
和payload
作为参数。可以看到store._mutations[type]
是一个数组,也就是说,同一个type
可以注册多个mutation
处理函数。如下例子:
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++
}
},
modules: {
inner: {
state: {
num: 0
},
mutations: {
increment(state) {
state.num++
}
}
}
}
})
store.commit('increment')
console.log(store.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
执行后,里外两层的mutation.increment
都会被调用,最后state的结果为:
{
count: 1,
inner: {
num: 1
}
}
2
3
4
5
6
7
(2)接着是遍历_rawModule.actions
注册Action:
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key // 转化key
const handler = action.handler || action // 获取action的handler
registerAction(store, type, handler, local)
})
2
3
4
5
registerAction
定义如下:
/**
* 注册Actions
* @param {Store} store Store实例
* @param {string} type action的type,可以是key,也可以是命名空间+key
* @param {Function} handler 处理函数
* @param {Object} local 局部上下文
*/
function registerAction(store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = []) // 初始化
entry.push(function wrappedActionHandler(payload, cb) {
// 以store绑定this,定义的上下文以及payload、cb作为参数调用处理函数
let res = handler.call(store, {
// 注入局部上下文的dispatch,commit,getters和state
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
// 注入根上下文的getters和state
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
// 返回结果不是一个thenable对象,则转为一个立即resolve的promise对象
if (!isPromise(res)) {
res = Promise.resolve(res)
}
// 安装了开发者工具插件
if (store._devtoolHook) { // 触发devtools的hook
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
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
actions
也会处理其命名空间,跟mutations
一样,同一个命名空间下的同名action也会被处理成数组。但与mutations
相比,actions
的处理有如下不同:
action
处理函数除了接收局部的state
外,还接收了局部的dispacth
、commit
和getters
和state
。同时还接收了根作用域下的rootGetters
和rootState
。这是因为在Vuex
的设计中,mutation
被设计成是同步的,它应该纯粹的用于操作当前上下文的state
,而不应该被用于跨模块的操作。而action
被设计成是异步的,在实际的处理逻辑中,可能会涉及多模块中间的数据处理。当然,实际的使用中也不是绝对的,但这是一种较好的实践。action
的返回结果会被处理成一个Promise
对象,所以Vuex
是依赖Promise
才能正常工作的。
注意
从源码中,可以看到action
的处理函数除了接收context
、payload
两个参数外,还接收了第三个参数cb
,这查看Vuex官方的文档中并没有该参数的相关说明。查看Vuex的迭代记录,发现其在v3.1.2
版本中,已经将cb
参数移除。
(3)紧接着是遍历_rawModule.getters
注册Getter:
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
2
3
4
registerGetter
定义如下:
// 注册getters
function registerGetter(store, type, rawGetter, local) {
if (store._wrappedGetters[type]) { // 已经注册过了
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// getter收集
store._wrappedGetters[type] = function wrappedGetter(store) {
// 传入局部上下文的state、getters,全局的state、getters依次传入作为参数调用getter函数
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
可以看到,getters
会将命名空间与key
计算出type
,然后通过store._wrappedGetters[type]
收集起来,收集起来的函数是一个接收store
实例参数,调用原始getter
函数的包装函数,原始getter
函数会依次接收局部上下文的state
、getters
,全局的store.state
、store.getters
作为参数调用。
(4)最后是递归安装子模块
到这里最外层的mutations
、actions
、getters
已经注册完毕,如果当前模块有子模块,即module._children
不为空,则递归安装子模块:
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
2
3
至此,installModule
函数就执行完毕了。此时,store
实例中的_mutations
、_actions
、_wrappedGetters
、_modules
、_modulesNamespaceMap
等属性都已经被初始化完毕。如下例子:
点击查看代码
const store = new Vuex.Store({
state: {
firstName: 'Foo',
lastName: 'Bar',
count: 0,
},
getters: {
fullName(state) {
return state.firstName + ' ' + state.lastName
}
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
AsyncIncrement(context) {
context.commit('increment')
}
},
modules: {
inner: {
namespaced: true,
state: {
num: 0,
age: 18
},
getters: {
person: (state, getters, rootState, rootGetters) => {
return {
firstName: rootState.firstName,
lastName: rootState.lastName,
name: rootGetters.fullName,
age: state.age
}
}
},
mutations: {
increment(state) {
state.num++
}
},
actions: {
AsyncIncrement(context) {
context.commit('increment')
}
},
}
}
})
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
结果:
此时,我们还无法通过store.state
和store.getters
来获取相关的响应式值,因为它们还未被设置到store
实例中。它们是通过resetStoreVM(store, state, hot)
来实现的,接下来我们来分析resetStoreVM
函数。
# 3. resetStoreVM函数
源码如下:
点击查看代码
function resetStoreVM(store, state, hot) {
const oldVm = store._vm // 获取旧的store._vm实例
// bind store public getters
// 在store上添加公共属性getters,并将其代理到store._vm实例上
// 这样就可以使用store.getters.xxx访问getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历store._wrappedGetters,将其添加到computed上
// 后续作为store._vm的计算属性传入
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// 使用计算属性实现getters的缓存和惰性求值机制
computed[key] = () => fn(store)
// 代理getters的属性访问到store_vm实例上的属性
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实例,这是store响应式的核心
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
// 开启严格模式,严格模式下不能在mutation外部修改state
if (store.strict) {
enableStrictMode(store)
}
// 对旧的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())
}
}
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
# 3.1 获取和定义定义相关属性
const oldVm = store._vm
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
2
3
4
首先会保存store._vm
实例,这是为了后续可以销毁旧的store._vm
实例,详细见后文详解。然后在store
实例上添加getters
属性,初始值为空对象,此操作也意味着如果store
实例上原本有旧的getters
属性,则会被覆盖。最后获取store._wrappedGetters
属性并定义computed
变量,用于后续代理getters
。
# 3.2 getters代理
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// 使用计算属性实现getters的缓存和惰性求值机制
computed[key] = () => fn(store)
// 代理getters的属性访问到store_vm实例上的属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
2
3
4
5
6
7
8
9
10
通过遍历store._wrappedGetters
,并将其添加到computed
上面,从上文我们知道,每一个wrappedGetter
的定义如下:
function wrappedGetter(store) {
// 传入局部上下文的state、getters,全局的state、getters依次传入作为参数调用getter函数
return rawGetter(
local.state,
local.getters,
store.state,
store.getters
)
}
2
3
4
5
6
7
8
9
computed
对应的函数中就会传入当前的store
实例。computed
在后续会被作为store._vm
实例的计算属性传入。
之后,会将store.getters
相关属性的访问代理至store._vm
实例上,这样,store.getters.xxx
就可以直接访问到store._vm
实例上的xxx
属性。
# 3.3 state响应式处理
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
2
3
4
5
6
7
8
9
这是Vuex
响应式的核心,store._vm
其实就是一个Vue实例,并将state
作为data
传入(data.$$state
),同时将computed
作为计算属性传入。也就是说,Vuex的getters
最终会被转为Vue实例的计算属性,即getters
是惰性求值的。
另外这里在定义store._vm
实例时,会设置Vue.config.silent
为true
,即忽略Vue实例化时的警告信息。这样做的原因是因为:这个Vue实例是Vuex用来响应式处理state
的,属于内部使用的实例,与用户业务上的Vue实例是不一样的。如果用户在全局定义了mixin
(包括但不限于使用Vue.mixin
和Vue.use
定义),且假如这些mixin
在Vue实例化的时候触发了警告,那么这些警告信息可能会干扰到开发者对Vuex
本身的调试与理解,从而感到困惑。为了避免这些全局的mixin触发的警告带来的问题,这里就临时将Vue.config.silent
设置为true
,隐藏掉这些警告。
除了getters
被代理到store._vm
实例的计算属性上以外,所有对state
的访问也会被代理到store._vm
实例上。其定义在Store
实例的访问器属性上:
get state() {
return this._vm._data.$$state
}
2
3
所以对store.state
的访问实际是对store._vm._data.$$state
的访问,即前面传入resetStoreVM
函数中传入的state
。此state
已经是处理过的具有嵌套结构的对象。
提示
这里我们还注意到了,store.state
是代理到store._vm._data.$$state
, 而不是store._vm.$$state
,这是因为在Vue中,以$
和_
开头的属性被当作内部属性来看待,是不会被代理到Vue实例上的。
# 3.4 开启严格模式
如果设置了严格模式,则会开启严格模式的处理:通过同步深度监听state的变化,判断当前的操作是否处于commit
操作中,如果不是,则抛出异常。
if (store.strict) {
enableStrictMode(store)
}
function enableStrictMode(store) {
// 同步深度监听_vm._data.$$state的变化
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
// 如果当前处于非commit状态,则抛出异常
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
注意
尽管开启了严格模式,在直接修改state
时,会抛出错误,但是此时state
的修改仍然是响应式的,这些修改也会触发视图更新。
# 3.5 销毁旧的store._vm实例
Vuex
是支持动态注册模块的,在动态注册模块的时候需要重新安装模块并更新响应式实例,所以需要销毁旧的store._vm
实例,避免内存泄漏等问题。
const oldVm = store._vm
// ...
store._vm = new Vue({
// ...
})
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
// 分发所有订阅的watcher,强制重新计算getter,以强制触发热更新。
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 销毁旧vm实例
Vue.nextTick(() => oldVm.$destroy())
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在热重载时,通过将store._vm实例的state设置为null,可以触发所有订阅的watcher, 触发getter的重新计算,进而触发热更新。
将oldVm
放到nextTick
回调中销毁是因为:此时有可能还存在基于旧实例的响应式依赖(如计算属性、监听器和渲染函数等),为了确保在销毁 oldVm 之前,所有基于 oldVm 的响应式依赖都已经完成了它们的更新和清理工作,所以放到nextTick
回调中销毁。
# 3.6 小结
resetStoreVM
函数主要完成了以下几件事情:`
- 为
store
添加_vm
属性,其值为一个Vue
实例 - 为
store
添加getters
属性,并将store._wrappedGetters
转为计算属性传递给store._vm
的computed
选项 - 将
state
作为store._vm
的data.$$state
选项传入 - 通过以上处理将
store.getters
相关属性的访问代理至store._vm
实例的计算属性上;结合store
的访问器属性,将store.state
代理值store._vm._data.$$state
上 - 严格模式的处理
- 销毁旧的
store._vm
实例
# 4. 总结
通过以上处理,其实Store
实例核心的初始化工作就已经全部完成了。其主要的工作就是根据传入的options
配置,安装模块并实现响应式机制,然后对外提供一套API。执行new Store(options)
的基本脉络如下图:
一个完整的Store
实例实例化后结果如下:
示例代码
const store = new Vuex.Store({
strict: true,
state: {
firstName: 'Foo',
lastName: 'Bar',
},
getters: {
fullName(state) {
return state.firstName + ' ' + state.lastName
}
},
mutations: {
increment(state, payload) { }
},
actions: {
AsyncIncrement(context) { }
},
modules: {
inner1: {
namespaced: true, // 开启命名空间
state: {
name: "inner1"
},
getters: {
something(state, getters, rootState, rootGetters) {
return state.hours * state.price
}
},
mutations: {
increment(state) { }
},
actions: {
AsyncIncrement(context) {
context.commit('increment')
}
},
},
inner2: {
// 未开启命名空间
state: {
name: "inner2"
},
getters: {
something(state, getters, rootState, rootGetters) {
return state.hours * state.price
}
},
mutations: {
increment(state) { }
},
actions: {
AsyncIncrement(context) {
context.commit('increment')
}
},
}
}
})
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
Vuex.Store
实例提供一些实例方法并作为API供外部调用,下一篇文章我们来介绍这些实例方法。