vuex 源码阅读
本文对应版本 b58d3d6a6426e901175a04bf6dcf206561cc82f5
获得代码
1 | git init |
总览
是个啥,状态管理工具
先了解如何用
前置知识
https://cn.vuejs.org/v2/guide/mixins.html
https://cn.vuejs.org/v2/guide/plugins.html
目录
1 | ./ |
依赖(表面的)
文件 | import 依赖 |
---|---|
mixin | |
util | |
helpers | |
store.js | 依赖见下方 |
index.js/index.esm.js | store.js & helpers.js |
plugins/devtool | |
plugins/logger | util:deepCopy |
module/module | util:forEachValue |
module/module-collection | module:Module , util:{ assert, forEachValue } |
注 之所以说是表面的,实际上比如logger是有调用store里面的方法的,比如subscribe
代码阅读
index/index.esm.js
主要是方法导出
1 | import { Store, install } from './store' |
mixin.js
主要是按照vue的mixin规则,根绝vue版本,向vue里注入 vuex,也就是$store
规则是,有this.$options.store则 this.$store= $options.store/store() 根据是不是函数
没有的话,用this.$options.parent.$store
1 | function vuexInit () { |
util.js
1 | export function find (list, f) //list中f函数返回true的第一个值 |
plugins/
devtool
目测是一些调试相关的钩子挂载,TODO
1 | const target = typeof window !== 'undefined' |
logger
嗯 第一行 // Credits: borrowed code from fcomb/redux-logger
借来的代码,依赖上 只用了util的deepCopy
基本上是输出 state状况用于调试的
module/
module
1 | Module: |
child 操作:对this._children
的增删
update(rawModule):对namespaced,actions,mutations,getters 有则覆盖的更新
1 | update (rawModule) { |
除此以外就是 在该class上 foreach再封装了
1 | forEachChild (fn) |
module-collection
export:
1 | export default class ModuleCollection { |
这边一个runtime有啥用,这里传的false,下面默不传是true,在Module里只是存一下,TODO
function makeAssertionMessage // 生成 assert错误message
使用
1 | const functionAssert = { |
来检测是否是个合法的RawModule
这里实现可以看到,rawModule也可以不含getters/mutations/actions,如果含有则需要分别是function/function/object(function / obj.handler == function)
然后发现一个有点意思的东西,之前没看过源码还不知道, process.env.NODE_ENV !== 'production'
才会assert
也就是开发环境才会assert,产品环境去掉
get:按照 从this.root开始,按照上面设计的Module Class的层级访问, this.root._children[path[0]]._children[path[1]]...
解释register
1 | register (path, rawModule, runtime = true) { |
靠外部传来的rawModule,作用是把 rawModule中描述的层级关系的modules,递归的解析成为内部实现的Module,这里的path只是配合内部实现的get,方便查找树状上的Module,是临时生成的一个传递变量
这里也可以看到,实现过程中重名的话是后者覆盖前者.
unregister (path) {//通过path定位 也就是上面get的办法,然后删除,需要满足 路径上的runtime都为true TODO
getNamespace (path) {
看起来像是 path.join('/')
实际上,是只会join有namespaced的每一层
update (rawRootModule) / function update (path, targetModule, newModule) {
这两个update的作用就是在已经建立Module树上,进行更新,其中如果遇到树的结构不同则忽略掉,更新的过程不会变更树结构,只会对namespaced、actions、mutations、getters进行替换(更新)
helpers
export:
1 | export const createNamespacedHelpers = (namespace) => ({ |
function normalizeMap (map)
把array,obj转化成[{'key':,'val':},...]
function normalizeNamespace (fn) { // 函数参数预处理(namespace,map) -> fn(namespace,map)
function getModuleByNamespace (store, helper, namespace) { return store._modulesNamespaceMap[namespace]
上面封了一层以后,每个map×××
被export出的都是单参数了
看了具体实现
1 | mapState: .vuex=true {支持devtools} |
除了getter,其它都是有namespace用namespace所指的,没有就用全局的this.$store
里的
getter 特殊在全是在this.$store.getters[]
里,如果有namespace则是namespace+val
构成新的val
Store
最后,最大的一个,505行
1 | import applyMixin from './mixin' |
export:
install && Class Store
先看install,也就是调用applyMixin(Vue)进行注入
Store上这里分析源码,省略一些错误判断的解释了,只要说说逻辑
Store:
1 | constructor (options = {}) { |
function unifyObjectStyle (type, payload, options) { //感觉是特殊处理type的情况 返回{type,payload,options}
makeLocalContext(store,namespace,path)
如果没有namespace 就用root上的
如果有则建立 本地化 的 dispatch commit getters 和 state
建立的实现过程 一个是调用unifyObjectStyle处理type的特殊情况,另一个就是给type 加上namespace
getters和state,使用defineProperties写到local上,注释说是因为它们会被vm更新,应该lazily的获取
其中getters的makeLocalGetters的原理是 namespace对比,和从store.getters[type]
中读取
makeLocalContext的结果会写入到module.context
installModule
如果当前路径有namespace, 那把它丢到store._modulesNamespaceMap[namespace]
和之前的获取对应
有点没看懂 set state,按理说 在 new ModuleCollection(options)就应该已经 在 new Module的部分设置了state,这里是为了触发Vue.set()?
1 | module.forEachMutation((mutation, key) => { |
整个install就是把 构建出的module 拆出所有的 操作函数
resetStoreVM
store._vm = new Vue({data:{$$state:state},computed})
其中 computed是 上面 提取出的_wrappedGetters
改动的,改动出的computed将 有和getters相同的keys,区别是 它会从store._vm[key]
的方式去读数据
在这一步设置_vm
时,临时设置Vue.config.silent
如果原来有._vm
则调用销毁
vuex API
至此 内部的基本结构就结束了,来看看调用api对应的实现
先看 官网的图 VUEX
store.state.xxx
1 | get state () { |
commit(type,payload,options)
这里实现的过程,通过解构出的this._mutations[type]
得到type对应的mutations 的函数句柄,
然后调用有withcommit包围的,进行 函数 执行
所有执行以后,对所有_subscribers
通知(mutation,new state)
, 简单的订阅模式
1 | grep -r "subscribe" * |
dispatch (type, payload)
同理 通过解构出的this._actions[type]
获得actions
区别是 action的订阅者 可以指定 before 和 after,分别会在action发生前后调用,
subscribeAction (fn)
如果fn是函数则 默认变为订阅action发生前 ,否则fn应该是类似{'before':()=>{},'after':()=>{}}
的格式
关于订阅的调用,目前只在测试里看到有,其它部分没有调用
除了环绕的订阅以外,就是执行对应的action,entry,如果是多个 调用Promise.all(),如果是一个直接entry0
剩余的
剩余的 hotUpdate,replaceState等 应该属于调试时使用的不属于核心功能了,watch 基本是靠的Vue的$watch
总结
- 整个源码阅读完后,才发现原来根本没有用到module这一部分,甚至都不知道,看来是没仔细读文档,原来这一块已经实现好了。
- 从Module部分可以看到,实现rawModule相关 过程是 加了一层套把rawModule套了一层,保护了树的结构,做到期望内的更新
- 潜在bug? 从外部传入rawModule 的第一次构建是
_rawModule=rawModule
,而在后面update的过程中是修改_rawModule
的字段,可能导致 期望外的修改? 不过根据代码,这一部分只会发生在hotUpdate
个人其它收获
- 实际实现的代码 除了实现想法外还有很多细节,这些细节感觉也蛮庞大?的
- 这样path的写法? 省一些指针?感觉每次get代价会大一些
- reduce 的用法(http://www.runoob.com/jsref/jsref-reduce.html)
- bind的用法(https://blog.csdn.net/kongjunchao159/article/details/59113129)
- es6 destructuring (https://www.deadcoderising.com/2017-03-28-es6-destructuring-an-elegant-way-of-extracting-data-from-arrays-and-objects-in-javascript/)
- get,defineProperties , 都是在调用时才计算,区别是get在原型上,而defineProperties 是在实例上(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Smart_self-overwriting_lazy_getters)
- 同时 其中实现,有些注释错误,有些函数的默认参数没有设置 =false,不过个人对js不太熟,要是c++的强迫症话 感觉还是要加的
- getters ,函数->计算属性
- 普通对象变化不会影响刷新,需要有getter和setter的,简单的方法就是用 new Vue({data:{xxx}});来包裹会自动加上 getter和setter
- 发布订阅
- strict通过 包裹限制 只有mutation改动数据, 但建议仅在开发时使用,在发布时为了性能不使用strict
其他:
感觉要去看看Promise的源码 以及 Vue的源码了
TODO
Module 里的runtime有啥用
重复注册state、mutation等等? 有文章说是 因为module的原因,
modules使用
modules{
模块名:{
state{
x:1,
y:1
}
},
模块名2:{
}
}
模块内的mutations,getters,actions 不同层级都会被调用!? 都会被合并到根上
所以要做的是方法映射出来,但是 把每个方法的所属模块的state绑定过去
但是 只有mutations和actions是数组,getters是由 defineProperties来的所以只会有一个
TODO
namespace:true 可以让模块化,不让actions等绑定在global上 通过module/module
路径访问,实现就是 根下的 map[ 拼接的module namespace路径实现的]
registerModule 动态注册模块
1 | registerModule (path, rawModule, options = {}) { |
store.subscribe() vuex中间件?? ,也是发布订阅模式
注册的是 和 函数指针放入数组,返回移出数组的函数,这个和vue-router里的registerHooks一样
commit后
1 | commit里 |