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 => ({ |