Vuex将所有的数据统一存放到内部store中,然后内部实例化一个vm监听store的变化,业务代码中使用到store时就会产生依赖。 业务代码中使用的各种mutation、action最终都是尝试修改内部的store状态,store真正变化时就会自动由Vue通知到业务组件。
入口
和其他Vue插件一样,Vuex也会提供一个install方法作为入口,位于store.js:
1 | function install(_Vue) { |
可以看到入口代码很简单,核心之处位于vuexInit方法,它是把我们在Vue实例化选项中的store对象放到了Vue.prototype.$store中,这样我们就可以在任何组件中使用this.$store了。而store选项是我们通过Vuex.Store构造函数生成的,例如:
1 | const store = new Vuex.Store({ |
Vuex几乎所有的核心逻辑都是Store了,相关的代码位于store.js中。这个文件初看起来很大,不过其实逻辑大多不难,耐心看下去应该没什么问题,而其中关于vuex module的篇幅又占了很多,随后我们会慢慢感知到。
Store
照常我们先从构造函数入口开始看起:
1 | constructor(options = {}) { |
里面有 3 个函数需要重点看下:
ModuleCollection: 由于Vuex可以利用module来模块化,这个类就是用于帮助我们构建一棵 module tree,每当有一个新 module 都会根据其对应的 path 来决定插入到 tree 的何处。整个 module tree 不会考虑 module 是否是 namespaced 的,root 对应的是 path 为空数组的那个installModule: 与ModuleCollection无关,主要用于处理层级 module,有些 module 会带有namespaced属性,此方法会专门进行处理resetStoreVM: 核心逻辑是将_wrappedGetters挂载到一个内部store._vm的computed上,然后定义store.getters公共api供业务代码使用,而store.getters其实就是取的store._vm.computed,这样就很巧妙的利用了Vue computed的懒惰计算了,每个getter只有在内部的依赖发生变化时才会重新计算,进而业务代码的相关属性也会享受到懒惰计算的好处。
ModuleCollection
先看看入口构造函数:
1 | constructor (rawRootModule) { |
只有在处理根 module 时会直接调用new ModuleCollection(options),所以只有 root module 的 path 才是空数组,再看看register:
1 | register (path, rawModule, runtime = true) { |
瞄一眼Module的构造函数:
1 | constructor (rawModule, runtime) { |
一些内部的帮助函数都很简单这里就不一一说了,整个register的逻辑就是构造一个module tree,每个 module 都会设置自己的state,然后会在installModule中用到这棵树。
installModule
这个函数非常核心,看看怎么实现的:
1 | function installModule(store, rootState, path, module, hot) { |
这个函数做的事情有点多,归纳一下有以下几点:
设置
_modulesNamespaceMap,这个对象的格式为{[namespace: string]: Module }将
module对应的state插入到state tree中,state tree以path为路径,不会考虑namespaced的影响将
localContext挂载到module.context上将
module.mutaions添加到store._mutations数组中,会考虑namespace。wrappedMutationHandler中的第 2 个参数state是local的将
module.actions添加store._actions数组中,会考虑namespace,store._actions中的每个函数会统一返回Promise。wrappedActionHandler中每个action前 4 个参数是local的将
module.getters添加到store._wrappedGetters数组中,会考虑namespace。_wrappedGetters中每个getter前 2 个参数是local的递归处理子
module
然后有些比较重点的子函数再挨个单独说一下。
getNamespace:
1 | function getNestedState(state, path) { |
要意识到path和namespace不是一一对应的,每一段path只有在其namespaced属性为true时才对应一段namespace. 例如path = ['a','b','c','d'],其中c对应的module为namespaced的,那么最终的namespace字符串为 'c/',而不是'a/b/c/d'.
makeLocalContext
如上所述,会生成局部dispatch, commit, getters and state。
1 | /** |
注意何时一个 module 对应的 namespace 为空?并不是只有根 module 的namespace是空串,只要子module的namespaced属性是空或 false,那么它就不会在namespace上贡献自己的力量,而是使用parent module的namespace。
全局的dispatch和commit其实定义在构造函数里,他们均硬绑定了store参数
1 | this.dispatch = function boundDispatch(type, payload) { |
local上的dispatch、commit在业务代码的调用参数上均和全局的dispatch一致,只不过真正执行逻辑时会在type上添加上namespace前缀,这就是他们的区别。local上的getter也会考虑namespace,最后生成的getters是快捷键形式的,在业务代码调用时不用写上namespace前缀,内部逻辑会帮忙添加。这块是借助makeLocalGetters做到的:
1 | // 从所有的getters找到那些key以namespace为前缀的,并生成短链形式的getters子集 |
local上的state也是局部的,只不过state是基于path的,而不会管namespace的影响。
registerMutation
将 module.mutaions 添加到 store._mutations 数组中
1 | function registerMutation(store, type, handler, local) { |
唯一需要注意的地方是第二个参数是local.state,而不是store.state
registerAction
1 | function registerAction(store, type, handler, local) { |
与mutation类似,真正执行的action前 4 个参数均是local的,另外经过wrappedActionHandler包装的action始终会返回Promise。
registerGetter
1 | function registerGetter(store, type, rawGetter, local) { |
_wrappedGetters最大的作用会在resetStoreVM中体现出来。
resetStoreVM
上面提到核心逻辑是将_wrappedGetters挂载到一个内部store._vm的computed上,然后定义store.getters公共api供业务代码使用。
1 | function resetStoreVM(store, state, hot) { |
粗略总结下这个函数做的事情:
将
store._wrappedGetters挂载到store._vm.computed上,以此实现响应式getter,调用computed[key]()会返回store._wrappedGetters[key](store),也就是rawGetter(local.state,local.getters, store.state, store.getters);将
state挂载到store._vm.data.$$state上设置调用
store.getters[key]会返回store._vm[key],即computed上的值
上面的enableStrictMode比较有意思,因为Vuex中规定业务方只能通过mutation来改变store,就是通过这个函数来做到的,核心是利用一个内部变量,只有在这个变量为 true 时,改变 state 触发的 watch 才不会警告:
1 | // 观察state是否在_committing之外被改动 |
至此,所有 vuex 内部的核心逻辑就全部看完了。接下来看一些Vuex的公共 api,了解其内部实现。
公共 api
commit、dispatch
最重要最常用的就是这俩了~~
commit
大体是找到所有符合条件的的mutations回调然后执行,内部利用_withCommit可以受到enableStrictMode的豁免。
1 | commit(_type, _payload, _options) { |
dispatch
与 commit 类似,大体是找到所有符合条件的的action回调然后执行。只不过因为action可以执行异步逻辑,这里会使用Promsie.all保证所有的action全部执行完成再返回结果。
1 | dispatch(_type, _payload) { |
registerModule
用于动态注册 module,会执行和Store构造函数类似的逻辑
1 | registerModule(path, rawModule, options = {}) { |
unregisterModule
用于动态卸载 module。
1 | unregisterModule(path) { |
resetStore
重置整个 store,会重新执行 installModule 和 resetStoreVM.
1 | function resetStore(store, hot) { |
mapXXX 系列
可以猜测到,这些帮助函数主要会利用到闭包来帮助我们处理跟namespace相关的琐碎细节。一起挨个看下。其中mapState、mapMutations、mapActions逻辑类似,这里只说其中一个。
mapState
经过它的处理,会返回一个对象,将mapState的参数转换为了 {[key:string]: ()=> any}形式的对象,可以直接利用它来当做组件的computed
1 | /** |
其中的normalizeNamespace函数,经过它的处理后,参数函数会拿到归一化之后的 namespace,''或者'a/b/c/'
1 | ** |
mapGetters
这个函数的大体逻辑和上面是类似的,只不过在细节上有可以说的地方。
1 | /** |
上面为什么是this.$store.getters和而不是和mapState类似的module.context.getters呢?
其实也可以那么写,不过要就要去掉上方的val = namespace + val了,因为module.context.getters是“短链”形式,不记得的看上面的makeLocalGetters。
那this.$store.getters是在什么时候赋值的呢?主要分为 3 步:
- 在
installModule中
1 | module.forEachGetter((getter, key) => { |
- 在
registerGetter中
1 | store._wrappedGetters[type] = function wrappedGetter(store) { |
- 在
resetStoreVM中设置store.getters
1 | forEachValue(wrappedGetters, (fn, key) => { |
经过这 3 步,store.getters就存放了所有”长链”形式 getter。 那么mapGetters中的this.$store.getters[val]就很容易理解了。
createNamespacedHelpers
很简单就是利用闭包绑定mapXXX的namespace:
1 | export const createNamespacedHelpers = namespace => ({ |