如果直接从源码的入口文件开始解读,并不能很好的理解消化,因为不知道 Vuex 的核心实现原理,会使读者很茫然,并不知道某段代码是做什么的。所以在这里,我首先对 Vuex 的实现原理进行解读。
Vuex 实现的核心其实是利用了 Vue 实例的响应式特性。我们在代码中通过 new Vuex.Store({}) 生成 Store 实例。 这个 Store 实例会被从上而下(父级到子级)的赋值到每个 Vue 实例对象的 $store 属性上,每个 Vue 实例都可以通过 this.$store 拿到这个对象,具体如何实现赋值到每个 Vue 实例的 $store 属性上的, 这个会在下篇文章介绍。在这里,你只需要知道每个 Vue 实例的 $store 属性都指向这个 Store 实例,而这个 Store 实例中有一个 _vm 属性,它是一个 Vue 实例,这个 Vue 实例是理解 Vuex 的关键之处,接下来请以 _vm 为中心来阅读这篇文章。
先从 state 说起。
Vuex 对 state 的处理首先是对各个模块的 state 对象根据其模块的嵌套规则组合成相互嵌套的对象,例如有下面所示的配置对象:
{
state: {
name: 'main module'
},
mutations: {},
getters: {}
modules: {
foo: {
state: {
name: 'foo module'
},
mutations: {},
getters: {}
},
bar :{
state: {
name: 'bar module'
},
mutations: {},
getters: {}
}
}
}
||
转换成
||
\/
{
name: 'main module',
foo: {
name: 'foo module'
},
foo: {
name: 'bar module'
}
}
然后在创建 _vm 的时候进行如下的操作:
// store 是 Store 的实例对象
store._vm = new Vue({
data: {
// state 就是上面转换完成的 state 嵌套对象
// 这样我们所编写的 state 就具有响应式的特性了
$$state: state
},
computed
})
我们平时从 Vuex 中获取 state 值是通过 this.$store.state.xxx 拿数据的,现在再来看看 $store 实例对象的 state 属性指向什么,相关代码如下:
class Store {
get state () {
return this._vm._data.$$state
}
}
从上面的代码可知:$store.state 指向的就是 $store._vm.data.$$state,和上面创建 $store 中的 $$state 正好对应。
总结:Vuex 将我们编写的配置对象中的 state 转换成以 state 嵌套的对象,然后使用它作为 data.$$state 的值创建 Vue 实例, 然后我们在自己的业务代码中获取的 $store.state 也是从这个 Vue 实例中取的,这个 _vm(Vue实例) 成为了业务代码和创建 Store 实例中的 state 之间的桥梁, 将两者链接起来,并且其是具有响应式特性的。
这主要看 store.js 中的 registerGetter 函数。
function registerGetter (store, type, rawGetter, local) {
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
通过上面的代码可知,我们写的 getter 函数被包装了一层,并被放到了 store 的 _wrappedGetters 属性中。key 值是 type, 这个 type 是命名空间和 getter 函数名的组合,例如命名空间是 'foo/bar',getter 的函数名是 'getFullName', 那么 type 的值就是 'foo/bar/getFullName',这只是处理的第一步。
接下来看看这些函数是如何被放置到 _vm 中的,看下面的代码:
// store.js
function resetStoreVM (store, state, hot) {
// 给 Store 实例设置 getters 对象
store.getters = {}
// 获取我们在 registerGetter 函数中设置的 _wrappedGetters,就像下面这个样子。
// _wrappedGetters:
// evenOrOdd: ƒ wrappedGetter(store)
// fooGet: ƒ wrappedGetter(store)
// joo/jooGet: ƒ wrappedGetter(store)
// joo/op/getFullName: ƒ wrappedGetter(store)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 对 wrappedGetters 进行遍历
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
// 这一段很有意思,我们给 store.getters 设置属性,key 是 getter 的路径加上 getter 名称,例如:joo/op/c1Get。
// 然后,设置的 get 从 _vm 中取值。这使得我们可以通过 this.$store.getters.xxx 取得 getter 值,并且是响应式的。
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
}
我们先从 store._wrappedGetters 拿到包含了 getters 的对象。对该对象进行遍历,遍历的回调函数做了两件事,一件事是将 getters 放置到 computed 对象中, 另一件事是将 getters 设置进 store.getters 中,这个 store.getters 就是我们在业务代码中使用 this.$store.getters 拿到的 getters, 这里比较关键的地方是其设置的 get 是 store._vm[key],也就是说我们在业务代码中使用 this.$store.getters.xxxx 拿的 getter 实际上是指向 _vm 中的 computed。 从下面的代码可知,遍历回调函数中被赋值的 computed 对象被用来实例化 _vm了,和上面的正好对应。
先列出相关的代码:
function registerGetter (store, type, rawGetter, local) {
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
到这里,还有一个关键之处没有说明,Vuex 给 rawGetter 函数指定的参数指向哪里?
这里比较好理解的是 store.state 和 store.getters,从上面的解读可知,store.state 和 store.getters 分别指向 _vm 中的 data.$$state 和 _vm 中的 computed。
这里比较奇怪的是 local 是什么?先列出与其有关的代码:
// store.js/makeLocalContext
Object.defineProperties(local, {
getters: {
get: noNamespace
// 在全局作用域中,
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
// store.state 是响应式的,因为他是从 this._vm.data.$$state 中取值的
get: () => getNestedState(store.state, path)
}
})
// 获取指定路径的模块的 state
function getNestedState (state, path) {
return path.reduce((state, key) => state[key], state)
}
其实 local.state 和 local.getters 最终还是指向 _vm 中的 data.$$state 和 _vm 中的 computed,只不过在这里进行了命名空间的处理,什么叫命名空间的处理? 我们以 local.state 为例,例如,我们假设有如下的 state 对象,这个对象会被放置到 _vm._data 中。
{
name: 'main module',
foo: {
name: "foo module",
tar: {
name: "foo/tar module"
}
},
bar: {
name: "bar module"
}
}
另外假设 tar 模块有如下的 getter:
printName(state, getters, rootState, rootGetters) {
rootState.name // main module
state.name // foo/tar module
}
从上面的输出可知,getter 的 state 其实指向的是 this.$store._vm._data.$$state.foo.tar,而 rootState 指向的是 this.$store._vm._data.$$state, 所谓的命名空间的处理就是将 local.state 指向 this.$store._vm._data.$$state 的更深层,指向该 getter 所在模块的 state(这句话是精髓),具体怎么实现的看 getNestedState 函数。 例如在本例中,printName 所在的模块是 foo/tar,它的 state 就指向 this.$store._vm._data.$$state.foo.tar。
看下面的代码:
function registerMutation (store, type, handler, local) {
// 首先判断store._mutations是否存在指定的 type,如果不存在的话给空数组
const entry = store._mutations[type] || (store._mutations[type] = [])
// 向 store._mutations[type] 数组中添加包装后的 mutation 函数
entry.push(function wrappedMutationHandler (payload) {
// 包一层,commit 函数调用执行 wrappedMutationHandler 时只需要传入payload
// 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
handler.call(store, local.state, payload)
})
}
从上面的代码可知,mutation 函数被包了一层,然后被 push 到了 store._mutations[type] 数组中,这个 type 是命名空间加上 mutation 函数名。
相关代码如下:
commit (_type, _payload, _options) {
// 获取 type 所对应的 mutation 函数数组
const entry = this._mutations[type]
// 遍历执行添加到 entry 数组中的包装函数
entry.forEach(function commitIterator (handler) {
handler(payload)
})
}
commit 函数的实现很简单,使用 type 从 this._mutations 中获取指定的函数数组,然后再遍历执行函数数组中的函数
我们在平时一般会这样书写 mutation 函数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
那么 increment 函数的 state 参数指向的是什么呢?
在这里,需要说明一点,虽然这个 mutation 函数是我们自己编写的,但是该函数的执行却是 Vuex 内部的操作,所以说这个 state 指向什么, 要看 Vuex 在执行这个函数的时候注入的是什么,看下面的代码。
function registerMutation (store, type, handler, local) {
// 首先判断store._mutations是否存在指定的 type,如果不存在的话给空数组
const entry = store._mutations[type] || (store._mutations[type] = [])
// 向 store._mutations[type] 数组中添加包装后的 mutation 函数
entry.push(function wrappedMutationHandler (payload) {
// 包一层,commit 函数调用执行 wrappedMutationHandler 时只需要传入payload
// 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
handler.call(store, local.state, payload)
})
}
这个 handler 就是我们所写的 mutation 函数,Vuex 在执行这个函数的时候使用的是 local.state,这个 local.state 在上面介绍过, 其实它指向 this.$store._vm._data.$$state 或者更深层的 state 对象。也就是说,我们编写的 state.count++,实际上是执行 this.$store._vm._data.$$state.count++,最终操作的对象还是 _vm 中的数据。
通过上面一系列的介绍,我们知道,当我们通过 this.$store.state.xxx 和 this.$store.getters.xxx 获取数据的时候,实际上是从 _vm 中获取数据, 因为 _vm 是 Vue 实例,所以我们拿到的数据都是响应式的。然后当我们通过 commit 改变数据的时候,其实改变的是 _vm._data.$$state 中的数据。 当 _vm._data.$$state 中数据改变的时候,我们使用了其中数据的页面也会响应式的更新。至此,就完成了 Vuex 中央状态管理仓库的功能。
Action 比较简单,和上面的理解思路一样,只不过多了一些 Promise 的处理。
相关代码和注释如下:
function registerAction (store, type, handler, local) {
// 首先判断 store._actions 是否存在指定的 type,如果不存在的话给空数组
const entry = store._actions[type] || (store._actions[type] = [])
// 和 registerMutation 一样,向 store._actions 中 push 包装过的函数
entry.push(function wrappedActionHandler (payload) {
// 这里对应 Vuex 官网的:https://vuex.vuejs.org/zh/guide/modules.html 中的 "在带命名空间的模块内访问全局内容(Global Assets)"部分
// action 函数的第一个参数是包含多个属性的对象,具体实现如下所示:
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 判断 action 函数的返回值是不是 promise,如果不是的话,将其包装成 resolved 状态的 promise,确保其返回值是 promise
if (!isPromise(res)) {
res = Promise.resolve(res)
}
})
}
相关代码和注释如下:
dispatch (_type, _payload) {
// 获取相应 type 的 action 函数数组
const entry = this._actions[type]
const result = entry.length > 1
// 如果 action 函数有多个的话,使用 Promise.all 进行处理
? Promise.all(entry.map(handler => handler(payload)))
// 只有一个的话,直接执行就行了,由于在 registerAction 函数中,注册的每个函数的返回值都进行了 promise 的包装,函数的返回值一定是 promise
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
// resolve 结果值
resolve(res)
}, error => {
reject(error)
})
})
}
相关代码和注释如下:
function registerAction (store, type, handler, local) {
// 首先判断 store._actions 是否存在指定的 type,如果不存在的话给空数组
const entry = store._actions[type] || (store._actions[type] = [])
// 和 registerMutation 一样,向 store._actions 中 push 包装过的函数
entry.push(function wrappedActionHandler (payload) {
// 这里对应 Vuex 官网的:https://vuex.vuejs.org/zh/guide/modules.html 中的 "在带命名空间的模块内访问全局内容(Global Assets)"部分
// action 函数的第一个参数是包含多个属性的对象,具体实现如下所示:
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 判断 action 函数的返回值是不是 promise,如果不是的话,将其包装成 resolved 状态的 promise,确保其返回值是 promise
if (!isPromise(res)) {
res = Promise.resolve(res)
}
})
}
handler 就是我们写的 action 函数,我们可以看到,参数还是蛮多的,不过好几个已经在前面讲过了,这里简要讲下 local.dispatch 和 local.commit,相关代码如下:
const local = {
dispatch: noNamespace ?
// 如果是全局命名空间的话,直接使用 store 中的 dispatch 函数
store.dispatch :
(_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
// 获取载荷形式的 type、payload 和 options
const { payload, options } = args
let { type } = args
// 如果 options 没有传递或者 options.root 为 false 的话,
// 说明这个 action 需要 dispatch 某一具体的命名空间(而不是全局命名空间)
if (!options || !options.root) {
// 往 type 前面拼接上命名空间
type = namespace + type
if (__DEV__ && !store._actions[type]) {
// 如果没有这个 action 的话,输出报错信息
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) {
type = namespace + type
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
其实 local.dispatch 和 local.commit 最终还是指向 store.dispatch 和 store.commit。只不过在命名空间存在的时候,进行了一些额外的处理, 处理的主要操作就是 type = namespace + type。为什么要进行这样的处理呢?这样处理有什么作用呢?这里以 commit 为例,假设有如下的 mutation:
let store = new Vuex.Store({
state,
getters,
actions,
mutations,
modules: {
foo: {
namespaced: true,
modules: {
bar: {
namespaced: true,
state: {
count: 0,
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAction ({dispatch, commit, getters, state, rootGetters, rootState}) {
// 看这里
commit('increment')
}
}
}
}
}
}
}
})
这个 increment mutation 函数所在的命名空间是 "foo/bar/",该函数保存在 store._mutations['foo/bar/increment'] 数组中。 当你执行 commit('increment') 的时候,其内部会执行 type = "foo/bar/" + "increment"; store.commit(type); store.commit() 函数就会执行 store._mutations['foo/bar/increment'] 数组中的函数。如果 Vuex 不进行 type = namespace + type 的处理的话,你必须在 23 行执行 commit("foo/bar/increment"),这非常的不方便,在库的设计上也不合理。 也许你已经感觉到了,通过执行 type = namespace + type,使 Vuex 有了一种 "局部 mutations" 的概念,就是当你 commit 当前模块的 mutation 函数的时候, 你直接 commit 这个 mutation 的函数名就可以了,不用管这个模块的命名空间,这种库的设计非常的好,很值得我们借鉴和学习。