// store.js
export class Store {
constructor () {
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.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
// ...
}
}
function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
下面从源码简单分析一下vuex的启动与使用过程,适合对
vuex
有使用经验看一下;适当回顾,也是对提高很有帮助。先看一下简略初始化的流程,后面看完再回头过一遍:
图中左边是主要的流程,右边是对某个流程的重要关联,也属于主流程
在
src
目录下的index.js
文件主要内容是:其中
mapXxx
这些是工具函数,暂不展开说明;vuex
的主要内容是在Store
中,就从这个文件为入口进行分析:安装挂载store
当调用
new Store()
的时候;先看第二个if
,这里通过判断当前运行的环境,例如在开发环境中,不是通过new
,不支持Promise
等情况进行warning提示在构造器的第一个
if
当中,帮助用户自动安装vuex
到vue
中,安装的主要函数是install
,我们继续来看这个函数:在
install
函数当中,在非生产环境当中,也会判断是否有重复安装;若没有重复安装,则调用applyMixin
方法。对于
applyMixin
的处理分两种情况,如果是大于vue@2.x
版本,会使用全局混入的方法处理,把初始化的过程加入到组件的beforeCreate
钩子;而对于vue@1.x
版本,会在原型链中添加_init
方法;这两种情况都是调用了vuexInit
的函数;vuexInit
的函数的处理是把store
挂载到每个组件的this.$store
中;大概的逻辑是,每个组件调用的时候,都会从父组件(this.$options.parent)
获取$store
,那样子就可以做到把$store
逐级往下传递:组件渲染也是从上层往下层渲染,例如上面的结构,根组件是通过
this.$options.store
获取$store
;foo
组件的父组件是root
,那么从root
中获取$store
的引用;而渲染到bar
,baz
的时候,就从foo
组件中获取。安装过程小结:在调用
new Store()
的时候,会判断当前环境支持情况,是否重复安装等情况进行判断,若不符合要求会报warning或error;安装的时候注入全局mixin
或者更改Vue.prototype
方法,对新增组件完成添加$store
的过程。根据配置生成模块
通过上一步安装Store之后,之后就根据用户传入的配置进行初始化。下面的代码片段仍然属于
Store
的构造器这一段的代码片段没什么特别,把一些属性挂载到
this
,也就是Store
实例;其中有一段this._modules = new ModuleCollection(options)
需要调用到外部class
生成:构造器中没什么特别,调用
register
方法,而传过去的方法是三个:path = []
模块的路径,根目录为空值 [],非根目录则为模块嵌套的路径:['foo', 'bar']rawModule = rawRootModule
rawRootModule 为该模块的配置,根目录的配置为用户传入的所有配置runtime = false
忽略掉环境的判断,可以看到还需要调用新模块方法:
new Module()
;这一块我们暂时忽略,知道是根据当前的配置生成一个模块,后面回头再看;由于初始化的
path
数组为空,因此只需要在ModuleCollection
的实例的root
属性添加新模块引用;执行到if (rawModule.modules)
,若根模块有modules
,则表示有子模块,就会遍历所有的子模块,同样调用register
;当子模块调用的时候,需要把子模块,挂载到父模块中。例如:对于这个配置,根目录下有
foo
与baz
模块,foo
模块有bar
模块;对于这种结构生成的ModuleCollection
实例简略信息为:至此,
ModuleCollection
已完成;我们回头看一下刚才new Module()
的处理:只是把构造函数的参数挂载到实例,其中
_rawModule
是指该模块对应的配置,mutations
,state
这些。小结:通过
new ModuleCollection()
传入用户的配置,调用register
递归把子模块注册完毕,形成模块间的父子关系,最后挂载到store._modules
属性中绑定commit与dispatch方法
对配置的模块关系处理完毕之后,就需要给
store
绑定commit
与dispatch
这里先提取原型链的
commit
和dispatch
方法,然后重新赋值,使用call
方法保证了commit
与dispatch
方法执行的上下文为store
的实例根据模块关系注册muations、actions、getters
来分析一下
installModule
,部分有写注释到代码;if (module.namespaced)
是用来判断是否有重名的模块,在非生产环境提示(真的很多提示...),然后来到关键一步:通过
makeLocalContext
函数,对当前模块对应的commit
与dispatch
再做一层处理,使得适配对子模块路径调用,例如commit('/foo/bar')
;对模块的state
与getters
做数据劫持处理;这个暂时先跳过,我们先知道这个函数的作用。接下来
installModule
就对mutations
,actions
,getters
进行注册,分别赋值到:mutations
=>store._mutations
actions
=>store._actions
getters
=>store._wrappedGetters
那么
store._actions
里面是什么?这是一个hash数据,key是action
的名称,value是这个action
的handler调用函数。为什么一个
action
对应多个handler?因为vuex对module处理的时候,如果模块没有明确声明namespaced: true
,那么这个handler获取到的state
也是'root'
下的数据这些handler有什么特别?为了传入更多的参数,例如我们调用的时候是:
store.dispatch('type')
,但在store
的action
,可以接收更多的参数;因为注册actions
的时候,再套一层函数,设置handler在调用传入参数。mutations
与actions
比较相似,允许多个同名,但是getters
只能允许有一个。当注册完以上数据的时候,则对所有子模块递归处理,那么子模块的
actions
等也处理完毕;最后得到所有的actions
;最终形成的数据:小结:通过
installModule
递归安装所有模块;通过makeLocalContext
获取到对应模块的上下文,使得commit
与dispatch
能够获取到子模块的数据;根据命名空间的设定,生成带路径的调用type
,分别挂载到store
对应字段;并对handler的参数进行调整。makeLocalContext
从上面可以知道,
makeLocalContext
是设定模块的上下文,处理过程如下:对
dispatch
的处理,如果没有指定命名空间,那么action
接收到的state
等是与root
下一致;如果指定命名空间,则对原来调用的type
进行拼接,加上对应的命名空间;对应上面所有的:store._actions
的hash的key值;对于
commit
的处理,与dispatch
一致;而对getters
和state
的处理,需要对数据进行劫持,延迟更新。至此,安装模块已经完毕,我们继续回到
Store
的构造函数resetStoreVM
resetStoreVM
主要做的是:getters
转换为store._vm
的计算方法;store
的state数据进行深度监听;对于直接修改state
数据的时候,进行错误提示。至此,
new Store()
的流程基本走完,还有插件的处理,这里就暂不展开说明。执行commit与dispatch
执行commit
上面说到,如果直接修改
state
的时候,会进行错误提示;那么为什么通过commit
就不会呢?从上面看到,
commit
的流程,就是从store._mutation
拿到对应的handler数组,然后逐个执行;而循环执行所有handler是包裹在_withCommit
方法里面:因为调用
_withCommit
的时候,会把标识位this._committing
设定为true
,执行中,会触发前面resetStoreVM
说到的,深度监听state
回调函数;该回调函数判断this._committing
为true
,则为正确的调用;_withCommit
把函数执行完毕之后把标识位重新设定为false
;这样子就可以做到,对不通过commit
调用的提示。执行 dispatch
vuex的异步操作,是放到
dispatch
调用:从代码可以看出,通过
dispatch
的type
找到所有的handler,如果handler只有一个,则直接返回这个handler的调用处理;若有多个,则使用Promise.all
包裹处理,最终作为一个Promise
形式返回;用户设定的actions
实际上并不会一定是Promise
,这种情况,只有一个handler会出错吗?不会,因为handler在
registerAction
的时候,对返回值进行判断,若不是Promise
,则直接返回Promise.resolve(res)
,res
为actions
的返回值。除了主流程的执行,还有执行前后对
subscriber
执行的try...catch
处理总结
简单分析了一下vuex的源码的主要流程,发现其中很多处理都很巧妙;例如,上下文,执行参数的处理;值得学习...但是文章对插件的分析过程缺失,大家也可以去熟悉一下,END.