![a5045477a5d597de892127a8739cd464.png](https://i-blog.csdnimg.cn/blog_migrate/5674b1d72af6b4f0f43a7d14526686c2.jpeg)
![5926b09fd5898c18e3c3244ca1064d46.png](https://i-blog.csdnimg.cn/blog_migrate/024b9c12e976e5a4f0390a11c4cc82f3.jpeg)
![bf56c0363d705355f63edbcef45d9114.png](https://i-blog.csdnimg.cn/blog_migrate/de71460fea08980a4cecf25d3a5d7ece.jpeg)
![5dc1d1aa3f858ea034645021e2340342.png](https://i-blog.csdnimg.cn/blog_migrate/f7caf2410b0364ebf25f4209c2a46b20.jpeg)
module
之所以先看Module
,是因为生成Module
的次序其实是在前,只有Module
在创建Vuex.Store
的时候准备好,才能安心的去commit
,去dispatch
,可以说创建Vuex.Store
的过程其实基本就是准备Module
的过程。
这一次用debug的方式来跟踪源码,先是不指定namespace
参数,我们准备一下,在examples
目录下新建一个deepinside
目录,然后加入如下module
模块文件
// moduleA.js
import moduleAA from "./moduleAA";
const state = {
count: 0
};
const mutations = {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
};
export default {
//namespaced: true,
state,
mutations,
modules: {
moduleAA
}
};
//moduleAA.js
const state = {
count: 0
};
const mutations = {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
};
export default {
//namespaced: true,
state,
mutations
};
//moduleB.js
const state = {
count: 0
};
const mutations = {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
};
export default {
//namespaced: true,
state,
mutations
};
//store.js
const state = {
count: 0
};
const mutations = {
increment(state) {
state.count++;
},
decrement(state) {
state.count--;
}
};
export default new Vuex.Store({
state,
mutations,
modules: {
moduleA,
moduleB
}
})
这样形成了一个树状的module
结构,注意我特意用了同样的mutations
名字
- store
- moduleA
- moduleAA
- moduleB
- moduleA
其实我已经默认了在module
配置的时候是可以用一个复杂的树状结构来表示的,接下来就是用debug见证奇迹的时刻了。
![060f90147dc7da6ab96054acead5ac80.png](https://i-blog.csdnimg.cn/blog_migrate/fb6231be84e3395012a86434e4868f73.jpeg)
在Store
的constructor
里打上断点,静悄悄的观察起来,注意这一句
this._modules = new ModuleCollection(options)
钻进去瞅一下
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
继续深入到register
方法
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)
})
}
}
一看就是递归组装树,也就是从
![0e3bb2e476dc3b10ddf3b1a8730a5d37.png](https://i-blog.csdnimg.cn/blog_migrate/5192754fbf213270667a65554dd68a85.jpeg)
生成了这样一个对象
![77f54508ec74ed51fa3b92c6f3719150.png](https://i-blog.csdnimg.cn/blog_migrate/8fa6b888ef1f8eaa138b6acc2e30c52f.jpeg)
其实变化并不大,只能称得上是一种Transform。
看看此时的store
对象,modules
已经准备好了,但许多其他的属性都还空着。
![08f1299c68c552c76c677f178883296a.png](https://i-blog.csdnimg.cn/blog_migrate/c33b9256f4cc1cc7190fb4be603b2193.jpeg)
状态数据也被抽到了一起
![152d211d7caed6ffcd36060d35647238.png](https://i-blog.csdnimg.cn/blog_migrate/84cbee492d4701d03bc76ace1600e6ad.png)
接下来看关键的一步,顺便翻译一下注释
// init root module. 初始化根module
// this also recursively registers all sub-modules 并且递归注册所有的子module
// and collects all module getters inside this._wrappedGetters 收集所有的getter
installModule(this, state, [], this._modules.root)
抛开检查、注释等语句,installModule
方法里主要是
// 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)
})
}
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);
});
- 准备
_modulesNamespaceMap
的内容 - 设置每一个非根部
module
的state
的内容,注意用的是Vue.set
语句,使其响应式了,并且是通过提交commit
(_withCommit()
)的过程形式进行的 - 递归把每个当前层的
module
的mutation
、action
·、getter
给准备好
抽一个forEachMutation
看看
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
整个installModule
方法跑完后再来看store
对象,mutations
已经准备好了。
![cf9fdcbb73ecffc0dbdd8f4bc00b2a81.png](https://i-blog.csdnimg.cn/blog_migrate/7ad0a10ae3e49ab19b231212d3f0bd4f.jpeg)
之前定义的mutation
都叫increment
和decrement
,这里把各个模块的都打平放到了一起,但它们并不相等,测试一下
this._mutations.increment[0] === this._mutations.increment[1] // 返回false
下一步也非常重要,也把注释翻译一下
// initialize the store vm, which is responsible for the reactivity 初始化store vm 响应变化
// (also registers _wrappedGetters as computed properties) 并注册_wrappedGetters为计算属性
resetStoreVM(this, state)
为什么要响应变化,因为在各个Vue
实例里都用到了$store
的数据,每当数据发生变化的时候,必须要通知到各个使用的地方。我的第一反应是通过Vue
实例,事实果然印证了我的第八感。
![2b8444193d98dcb4ef3fb5c0100682d4.png](https://i-blog.csdnimg.cn/blog_migrate/140a4646420a27d7f533953429679bcd.jpeg)
之前已经创建了名为_wrappedGetters
的对象,是在registerGetter
里。
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
的getter
的用法和Vue
的computed
用法是很相似的,所以在getter
上也是要建立响应机制的。再到resetStoreVM
方法里
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
把getter
和store._vm
建立起关联,而store._vm
马上就来
store._vm = new Vue({
data: {
$$state: state
},
computed
})
整个state
都放入了Vue
实例中,每当state
发生变化时,就是通知到使用它的地方做更新,而在store
上暴露出去的state
就是这里的_vm._data.$$state
。
get state () {
return this._vm._data.$$state
}
源码里还缓存了原来的_vm
,这是在热更新或者动态注册module
的时候会用到。
const oldVm = store._vm
//...
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
最后就是应用插件了
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
回顾一下整个的步骤
- 如果没有通过插件形式注入
mixin
,那么先注入它,保证每个Vue
实例都能拿到$store
- 初始化一系列的内部对象,其中最重要的是
_modules
,它其实是传入的options
里module
部分映射的对象 - (不设
namespace
时)把module里的mutation
、action
·、getter
都抽出来,打平放一起,不用管原来它们属于哪个module
,按名字分配到同一个数组里即可 - 为数据响应做好准备
- 应用配置的插件
注意刚才我们构造的是特殊场景,即所有的module都配置了相同名称的mutation。写一个组件来测试一下
<template>
<div id="app">
{{$store.state.count}}
<button @click="increment">+</button>
</div>
</template>
<script>
export default {
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state)
}
}
}
</script>
执行后发现,所有module
上的count
都被加了1,通过debug后发现,Store
接到commit
后,在_mutations
里找到了名为increment
的数组,每一个都执行了一遍,导致所有module
上的count
都被加了1,这也即是namespace
诞生的原因。
在module
配置里有一项namespace
的属性,现在我们在每个module
配置里把它加上并设置为true。
export default {
namespaced: true,
state,
mutations,
modules: {
moduleAA
}
};
此时再来观察一下生成的_mutations
数组
![98bef06886566d3a87d89df195a2a426.png](https://i-blog.csdnimg.cn/blog_migrate/3b9532ed999d9a6aa1678c5eff43bc87.png)
发现每一个mutation
都用path + action name
来作为key,这样子就减少了冲突。当我们再次执行this.$store.commit('increment')
的时候就只针对了根部的module
了。
![528f428802b8b3896104ad01d9ce0908.png](https://i-blog.csdnimg.cn/blog_migrate/c9ba0dde288b5bae386889c3294f5a70.png)
而带namespace
的场景下子module
的分发commit
默认只针对了该module
,详情在后面章节里会有介绍。
Vuex
在内部其实已经做好了检查,在installModule
里
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
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(() => {
if (process.env.NODE_ENV !== 'production') {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
}
针对namespace
和module
的名字是否重复,都做了检查与提醒。Redux
里针对同名的action
,并没有做特殊的处理,可以见我之前的文章https://zhuanlan.zhihu.com/p/37671649。不过在dva
里,还是考虑了把namespace
放入action
作为前缀。
第二个问题来了,如果我们直接修改Store
的state
是否可行,我的第一反应是可以的,因为从刚才的源码上也看出来了,Vuex
已经做了响应式,它把内部的state
放入了Vue
实例的data
中,一旦有Vue
实例,那么就自然而然的有响应式变化了。
继续做实验,直接修改state
// 在组件里直接修改state值
increment() {
this.$store.state.count = 100
}
结果当然是界面上数值更新为100,那么使用commit来提交变化是否还有意义呢?
答案当然是有的,使用commit
提交修改状态数据的话,有助于做手脚,比如打日志,跟踪数据变化,关联上浏览器开发调试插件等,或者其他一些想做的事情。如果你想要更灵活自定义一些操作,还有plugin
或者subscribe
等方式可选。
Vuex
也有一个strict
的配置项,如果设置为true的话,直接修改state
将会抛错。
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 })
}
动态module
动态注册主要是使用registerModule
方法
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
也能用unregisterModule
把动态注册的module
给卸载掉
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}
this._modules.register(path, rawModule)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)
}
unregisterModule (path) {
if (typeof path === 'string') path = [path]
if (process.env.NODE_ENV !== 'production') {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
this._modules.unregister(path)
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
}
对于注册和卸载,都是要再走一下初始化时的那些流程的,比如卸载的时候在挪走了模块、解除了上下关联关系之后,会调用resetStore
方法
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
搭车说一下热重载hotUpdate
方法,它内部也主要是调用了resetStore
重新整一遍内部的数据属性。