前言 开个坑,最近看的Vuex@next(4.0)
的源码,源码不复杂,可以拿出来写写
当作一个笔记,如果帮助到你理解了,那么我会相当开心😘
源码不是很复杂,文件也不多,但是我感觉写的小巧,很棒
应该会分成几个帖子来写,主要按照每个文件来写,如果有更好地编写方式,可以留言给我,因为对我来说,分文件写起来有条例以及可以循序渐进
Vuex
,是Vue
官方的一个全局状态管理,可以理解成一个全局的data
(类似组件里面的data
属性)
Vuex
也有暴露一些自己的方法,比如类似组件的计算属性(getter
),同步的修改数据操作(mutation
),支持异步方式修改数据的操作(action
)
个人觉得,如果想要理解源码,最好是先读它的API
,知道了它的API
是干什么的,看起源码来才能更好地理解
应该会分成几个部分来写
store.js
入口文件,创建一个store
的核心文件
module-colleton.js
模块列表对象,使得我们可以注册(register
)(卸载(unregister
))一个模块,嵌套模块
module.js
模块对象,每个模块的对象类
helper.js
工具函数,比如mapGetters
,mapMutations
等等
Vuex@next(4.0)仓库 Vuex@next(4.0)文档仓库
PS:如果直接搜索记得切换分支!!!如下图
vuex.vuejs.org
的文档还是Vuex3
的,不过Vuex4
暴露的API和Vuex3
的基本一样
所以如果想了解API
的作用看Vuex3
的文档问题不大
如果想看用例的,就取上面的文档仓库上找对应API
的MD
文件即可
也可以及直接 点我 就可以看到几乎全部的API
了
Vuex
的项目结构不复杂(PS:报红的原因是我没有安装依赖)
So,我还是有信心讲好的~ ok,那么我们开始吧~~
store.js
本篇主要讲建立一个store
的整体流程。
由于在Vue3
中开始推行setup
函数来进行逻辑的编写,有一点ReactHook
的味道,比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div > <button @click ="ageIncrementHandler" > 增加1岁</button > <p > 名字: {{ person.name }}</p > <p > 岁数: {{ person.age }}</p > </div > </template > <script > import {reactive} from "vue" ;export default { name : "Test" , setup ( ) { const person = reactive ({ name : "lwf" , age : 1 }); const ageIncrementHandler = ( ) => { person.age ++; }; return { person, ageIncrementHandler }; } }; </script >
运行效果如下
在Vuex3
中,也有一点这种味道,我觉得主要体现在创建store
上
Vuex3
创建store
1 2 3 const store = new Vuex .Store ({ })
到了Vuex4
,创建store
改为函数式创建
1 2 3 4 const store = createStore ({ })
这其中难道有什么魔法???
其实并没有,在store.js
中可以看到createStore
的源码
1 2 3 export function createStore (options ) { return new Store (options) }
无非是包了一层函数导出而已
Vuex4
在设计时兼容了Vuex3
,在4.0
的仓库的README.md
中的起始部分可以看到如下描述
This is the Vue 3 compatible version of Vuex. The focus is compatibility, and it provides the exact same API as Vuex 3, so users can reuse their existing Vuex code with Vue 3.
大意就是Vuex4
兼容了Vuex3
,暴露了和Vuex3
相同API
,使得在旧的Vuex
代码可以使用在Vue3
上。
真正的核心代码为下面的Store
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export class Store { constructor (options = {} ) {} install (app, injectKey) {} get state () {} set state (v) {} commit (_type, _payload, _options) {} dispatch (_type, _payload) {} subscribe (fn, options) {} subscribeAction (fn, options) {} watch (getter, cb, options) {} replaceState (state) {} registerModule (path, rawModule, options = {}) {} unregisterModule (path) {} hasModule (path) {} hotUpdate (newOptions) {} _withCommit (fn) {} }
这里省略了实现的代码,为了从宏观上去认识这个类
install
1 2 3 4 5 6 export class Store { install (app, injectKey) { app.provide (injectKey || storeKey, this ) app.config .globalProperties .$store = this } }
安装函数很简单
通过Vue3
的provide
API注入了自身,并且把自身挂载到app.config.globalProperties
的$store
上
provide
注入,这样在setup
函数中就可以使用useStore
(Vuex4
暴露的API
)来获取store
对象,注意:在setup
函数中没有this
!!!
globalProperties
挂载,使得Vue2
的方式中可以通过this
来获取store
,这种方法取代了Vue2
的Vue.prototype.propertyName = value
,比如之前想挂载经过axios
封装的API
层,一般如下写
1 2 3 import http from './http' ;Vue .prototype .$http = http;
然后在组件中
1 2 3 4 5 6 export default { mounted ( ) { console .log (this .$http ); } };
而在Vue3
中,只需要挂载到app.config.globalProperties
即可有相同的效果
1 2 3 4 5 import http from './http' ;const app = createApp (App )app.config .globalProperties .$http = http; app.mount ("#app" );
state
的get
,set
函数我们知道,Vuex不允许我们直接更改state
状态,必须通过commit
一个mutation
或者dispatch
一个action
来更改状态
从它源码可以看出,state
属性不是真正存放状态的地方,只是对外暴露的一个接口,通过定义get
和set
来限制用户的行为
1 2 3 4 5 6 7 8 9 10 export class Store { get state () { return this ._state .data } set state (v) { if (__DEV__) { assert (false , `use store.replaceState() to explicit replace store state.` ) } } }
可以看到get
返回了真正的状态的一个属性,位于_state.data
而set
则直接报错(开发模式__DEV__
下),提醒用户不要直接修改状态,而是要使用replaceState
这个函数来修改状态。
比如我们创建了下面这样的store
1 2 3 4 5 6 const store = createStore ({ state : { name : "lwf" , age : 22 } })
可以打印下store
,确实出现了_state.data
,并且是一个Proxy
对象(其实这是一个响应式的对象,后面会写到)
当然可以直接的修改这个对象,并不会出现什么错误,我们直接修改_state.data
1 2 3 4 5 6 7 8 store._state .data .age = 23 ;
这里要注意,set
只是对设置state
这个操作拦截
而无法递归的拦截,所以store.state.age = 23
从逻辑上并不会执行state
的set
,而是执行state
的get
打印看看
发现确实改变,也没有出现任何错误(这里没报错是因为默认情况下严格模式是关闭的)
当然这样子修改状态是不应该出现在实际的代码中的,因为这会使得状态的改变变得混乱。
constructor
新建一个store
时的初始化过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 export class Store { constructor (options = {} ) { if (__DEV__) { assert (typeof Promise !== 'undefined' , `vuex requires a Promise polyfill in this browser.` ) assert (this instanceof Store , `store must be called with the new operator.` ) } const { plugins = [], strict = false } = options this ._committing = false this ._actions = Object .create (null ) this ._actionSubscribers = [] this ._mutations = Object .create (null ) this ._wrappedGetters = Object .create (null ) this ._modules = new ModuleCollection (options) this ._modulesNamespaceMap = Object .create (null ) this ._subscribers = [] this ._makeLocalGettersCache = Object .create (null ) const store = this const { dispatch, commit } = this this .dispatch = function boundDispatch (type, payload ) { return dispatch.call (store, type, payload) } this .commit = function boundCommit (type, payload, options ) { return commit.call (store, type, payload, options) } this .strict = strict const state = this ._modules .root .state installModule (this , state, [], this ._modules .root ) resetStoreState (this , state) plugins.forEach (plugin => plugin (this )) const useDevtools = options.devtools !== undefined ? options.devtools : true if (useDevtools) { devtoolPlugin (this ) } } }
最前面的两个assert
判断表明Vuex
需要Promise
支持以及必须通过new
来创建
可以看到初始化了很多的属性
strict
严格模式的开启标志
_committing
跟严格模式相关,使得即使开启严格模式下也可以修改状态_state.data
_actions
存放所有的action
_actionSubscribers
存放所有的注册action
回调函数
_mutations
存放所有mutation
_subscribers
存放所有的注册mutation
回调函数
_wrappedGetters
存放所有的绑定参数的getter
_modules
嵌套的模块列表
_modulesNamespaceMap
带命名空间的模块列表
_makeLocalGettersCache
带命名空间的模块的所有getter
的缓存
在初始化了属性之后,绑定了dispatch
和commit
的context
1 2 3 4 5 6 7 8 const store = this const { dispatch, commit } = this this .dispatch = function boundDispatch (type, payload ) { return dispatch.call (store, type, payload) } this .commit = function boundCommit (type, payload, options ) { return commit.call (store, type, payload, options) }
为啥要这样写呢,原因就是如果我们把对象的函数赋值给一个变量,那么此时的this
会丢失,比如
1 2 3 4 5 6 7 8 9 10 const o = { name : "lwf" , say ( ) { console .log (this .name ); } } o.say (); const say = o.say ;say ();
绑定了上下文,这样用户如果使用解构之类的操作,也不会造成上下文的丢失
1 2 3 4 const { dispatch, commit } = store;commit (...) dispatch (...)
然后初始化了strict
严格模式标志,并且严格模式默认不开,从对options
的解构可以看出,strict
的默认值为false
1 2 3 4 5 6 7 8 const { plugins = [], strict = false } = options this .strict = strict
对于严格函数,可以先看enableStrictMode
这个函数(注意,这个函数不在类中,同一个文件内往下拉可以找到)
1 2 3 4 5 6 7 function enableStrictMode (store ) { watch (() => store._state .data , () => { if (__DEV__) { assert (store._committing , `do not mutate vuex store state outside mutation handlers.` ) } }, { deep : true , flush : 'sync' }) }
可以看到这个函数内部使用了Vue3
的watch
API
监听了store._state.data
这个对象,配置为deep
(深层监听)并且执行为sync
(同步的)
在回调函数内部,开发模式__DEV__
下会根据store._committing
状态来判断是否要抛出错误
我们可以试试配置下strict
为true
,然后打印来看看效果
1 2 3 4 5 6 7 8 const store = createStore ({ strict : true , state : { name : "lwf" , age : 22 } }); store._state .data .age = 23 ;
报错了,但数据还是更改了(所以还是要根据Vuex
的设计理念来,而不是直接修改数据)
有一个函数和enableStrictMode
存在关系,就是在这个类中的_withCommit
方法
1 2 3 4 5 6 7 8 9 10 11 export class Store { _withCommit (fn) { const committing = this ._committing this ._committing = true fn () this ._committing = committing } }
可以看到,这个函数使得内部修改_state.data
不报错(即使在严格模式下,不过现在还没写到它在什么地方开启)
因为_committing
被置为true
接着执行了两个函数installModule
和resetStoreState
1 2 3 const state = this ._modules .root .state installModule (this , state, [], this ._modules .root )resetStoreState (this , state)
在installModule
上有一行注释
init root module.this also recursively registers all sub-modules and collects all module getters inside this._wrappedGetters
大意就是,初始化根模块,递归注册所有子模块,收集所有模块的getters
到_wrappedGetters
下
在resetStoreState
上有一行注释
initialize the store state, which is responsible for the reactivity (also registers _wrappedGetters as computed properties)
大意就是,初始化store
的状态,通过reactive
API使之成为响应式的(reactive
)
并且也会注册wrappedGetters
里面的所有getter
到store.getter
下,使之成为一个计算属性(computed
)。
(这两个函数后面写,先认识总体的流程,如果想查看,点击右侧相应的部分即可跳转)
遍历插件数组,安装插件
1 2 plugins.forEach (plugin => plugin (this ))
对于插件的安装,并没有什么魔法,传入了自己作为参数,然后执行,仅此而已。
Vuex
的文档上给了一个简单的例子
1 2 3 4 5 6 7 const myPlugin = store => { store.subscribe ((mutation, state ) => { }) }
内部自带的logger.js
也是通过这种方式进行安装,文件位于plugins
文件夹下
1 2 3 4 5 6 7 8 export function createLogger ( ) { return store => { } }
最后是判断是否安装开发工具插件的代码(这个插件依赖了调试工具暴露的对象,实现其实非常简单,但这里忽略不写,因为和核心实现没有太大关系)
1 2 3 4 const useDevtools = options.devtools !== undefined ? options.devtools : true if (useDevtools) { devtoolPlugin (this ) }
commit
commit
用于提交一个mutation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export class Store { commit (_type, _payload, _options) { const { type, payload, options } = unifyObjectStyle (_type, _payload, _options) const mutation = { type, payload } const entry = this ._mutations [type] if (!entry) { if (__DEV__) { console .error (`[vuex] unknown mutation type: ${type} ` ) } return } this ._withCommit (() => { entry.forEach (function commitIterator (handler ) { handler (payload) }) }) this ._subscribers .slice () .forEach (sub => sub (mutation, this .state )) if ( __DEV__ && options && options.silent ) { console .warn ( `[vuex] mutation type: ${type} . Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } } }
unifyObjectStyle
对入参进行标准化
1 2 3 4 5 6 const { type, payload, options } = unifyObjectStyle (_type, _payload, _options)
unifyObjectStyle
规范了参数,在Vuex
中,commit
一个mutation
的方式有多种,比如
1 2 3 4 5 6 7 8 9 10 store.commit ("mutation1" , { }, {}); store.commit ({ type : "mutation1" }, {});
unifyObjectStyle
实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 function unifyObjectStyle (type, payload, options ) { if (isObject (type) && type.type ) { options = payload payload = type type = type.type } if (__DEV__) { assert (typeof type === 'string' , `expects string as the type, but found ${typeof type} .` ) } return { type, payload, options } }
如果第一个参数是对象,那么第二个参数就应该是options
,mutation
的名字应该为第一个参数的type
,载荷就是第一个参数。
如果第一个参数不是对象,那么就是三个参数的情况,直接返回即可
注意到中间还判断了mutation
的名字一定要是字符串,不然报错
接着从_mutations
属性中拿出了对应type
的函数数组
是一个数组,也就是说一个名字为m1
的mutation
是可以对应多个函数的
1 2 3 4 5 6 7 8 9 const mutation = { type, payload }const entry = this ._mutations [type]if (!entry) { if (__DEV__) { console .error (`[vuex] unknown mutation type: ${type} ` ) } return }
然后执行了_withCommit
来修改状态(防strict
为true
下报错)
对每个mutation
传入了载荷(自定义的参数),注意这里是一个完全同步执行的过程
1 2 3 4 5 this ._withCommit (() => { entry.forEach (function commitIterator (handler ) { handler (payload) }) })
然后进行回调的执行
1 2 3 this ._subscribers .slice () .forEach (sub => sub (mutation, this .state ))
注意这里调用了slice
返回了一个浅复制的副本
这么做为了防止在注册的函数中执行取消注册而造成的逻辑混乱问题(Redux
内部也有相似的逻辑)
最后一个判断和开发工具相关,忽略
dispatch
dispatch
用于分发一个action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 export class Store { dispatch (_type, _payload) { const { type, payload } = unifyObjectStyle (_type, _payload) const action = { type, payload } const entry = this ._actions [type] if (!entry) { if (__DEV__) { console .error (`[vuex] unknown action type: ${type} ` ) } return } try { this ._actionSubscribers .slice () .filter (sub => sub.before ) .forEach (sub => sub.before (action, this .state )) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in before action subscribers: ` ) console .error (e) } } const result = entry.length > 1 ? Promise .all (entry.map (handler => handler (payload))) : entry[0 ](payload) return new Promise ((resolve, reject ) => { result.then (res => { try { this ._actionSubscribers .filter (sub => sub.after ) .forEach (sub => sub.after (action, this .state )) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in after action subscribers: ` ) console .error (e) } } resolve (res) }, error => { try { this ._actionSubscribers .filter (sub => sub.error ) .forEach (sub => sub.error (action, this .state , error)) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in error action subscribers: ` ) console .error (e) } } reject (error) }) }) } }
dispatch
的参数传入方式和commit
基本一样,所以函数前面的逻辑也是规范参数,提取相应的部分
然后调用了那些注册的before
的函数,这里用try-catch
,防止before
函数执行出现错误导致后续dispatch
操作执行失败
1 2 3 4 5 6 7 8 9 10 11 try { this ._actionSubscribers .slice () .filter (sub => sub.before ) .forEach (sub => sub.before (action, this .state )) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in before action subscribers: ` ) console .error (e) } }
对于注册在action
的回调,有三种方式来执行回调
1 2 3 4 5 6 7 8 9 store.subscribeAction (() => { }) store.subscribeAction ({ before : () => {}, after : () => {}, error : () => {} })
所以上面的 .filter(sub => sub.before)
逻辑把before
的函数给筛了出来
注意这里也做了一个slice
浅拷贝,防止函数执行过程中取消注册(或者说是“取消订阅”)
1 2 3 const result = entry.length > 1 ? Promise .all (entry.map (handler => handler (payload))) : entry[0 ](payload)
然后执行了相应的action
函数,这里和mutation
一样,都是执行了一个数组,也就是存在多个的action
这里可能有疑问,为啥多个action
函数的时候就包了一层Promise.all
,一个的时候就没有呢?
这是因为在注册action
的时候(registerAction
这个函数,后面会写到它的实现),已经对action
进行包装了,使得每个action
一定会返回一个Promise
最后返回了一个Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 return new Promise ((resolve, reject ) => { result.then (res => { try { this ._actionSubscribers .filter (sub => sub.after ) .forEach (sub => sub.after (action, this .state )) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in after action subscribers: ` ) console .error (e) } } resolve (res) }, error => { try { this ._actionSubscribers .filter (sub => sub.error ) .forEach (sub => sub.error (action, this .state , error)) } catch (e) { if (__DEV__) { console .warn (`[vuex] error in error action subscribers: ` ) console .error (e) } } reject (error) }) })
action
和mutation
的不同之处就是action
里面支持异步的操作,而mutation
里面修改state
一定是同步的
所以dispatch
返回了一个Promise
,在action
执行完毕可以通过then
注册一个回调
1 2 3 dispatch ('action1' ).then (() => { })
dispatch
返回的Promise
很简单,根据action
返回的Promise
状态来决定如何解决dispatch
返回Promise
的状态
成功回调执行了注册为after
的函数,错误回调执行了注册error
的函数
简单点讲,就是包了一层Promise
,为了能够执行注册的after
和error
函数(3.4.0
版本新增的API
)
subscribe
注册在mutation
执行之后执行的函数
1 2 3 4 5 export class Store { subscribe (fn, options) { return genericSubscribe (fn, this ._subscribers , options) } }
可以看到,实现依赖了另一个函数genericSubscribe
,注册mutation
和action
都依赖了这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 function genericSubscribe (fn, subs, options ) { if (subs.indexOf (fn) < 0 ) { options && options.prepend ? subs.unshift (fn) : subs.push (fn) } return () => { const i = subs.indexOf (fn) if (i > -1 ) { subs.splice (i, 1 ) } } }
先判断函数fn
是否已经在subs
中了,不存在才放进去
options
的prepend
可以指定怎么把函数放到数组中
如果prepend
为true
,那么执行unshift
放到数组头部,反之push
到数组尾部
返回了一个函数,这个函数逻辑也很简单,就是找到数组里面的这个函数,然后通过splice
删除,也就是取消注册(取消订阅)
subscribeAction
注册一个函数,在action
执行之前before
(默认),执行之后after
和执行出错error
时执行
1 2 3 4 5 6 export class Store { subscribeAction (fn, options) { const subs = typeof fn === 'function' ? { before : fn } : fn return genericSubscribe (subs, this ._actionSubscribers , options) } }
可以看到先判断第一个参数是否为函数来判断是否包装为一个对象
如果直接传入了一个函数,也就是typeof fn === 'function'
为真,那么包装成一个{ before: fn }
并且也支持options
的prepend
来配置前插入还是后插入数组
genericSubscribe
的实现在subscribe
有写,这里就不重复了
watch
1 2 3 4 5 6 7 8 export class Store { watch (getter, cb, options) { if (__DEV__) { assert (typeof getter === 'function' , `store.watch only accepts a function.` ) } return watch (() => getter (this .state , this .getters ), cb, Object .assign ({}, options)) } }
watch
的实现依赖了Vue3
的watch
API,做了个简单的入参判断,来对状态进行监听以及执行传入回调,options
和Vue3
的watch
配置一样
replaceState
这个API官方给的信息太少,就一句
替换 store 的根状态,仅用状态合并或时光旅行调试。
1 2 3 4 5 6 7 export class Store { replaceState (state) { this ._withCommit (() => { this ._state .data = state }) } }
实现非常的简单,就是包在_withCommit
中执行,替换state
而已
一般为了在刷新时恢复状态使用,在beforeunload
事件中把状态序列化为字符串(JSON.stringify
)存入localStorage
中
然后在加载之后判断localStorage
是否存在,存在就使用这个函数来替换状态。
个人觉得很少会使用到这个API
…
因为如果是上面的情况,为什么不在创建store
之前就构建传入createStore
的参数呢?
registerModule
注册一个动态模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export class Store { registerModule (path, rawModule, options = {}) { if (typeof path === 'string' ) path = [path] if (__DEV__) { 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 ) resetStoreState (this , this .state ) } }
可以看到,注册的逻辑是由_modules.register
实现,注册完之后通过installModule
安装对应的module
然后执行resetStoreState
来重置store
来更新getters
在Vuex
中,在初始化设置的模块,可以理解为静态模块,这种模块无法删除
在运行过程中可以动态的注入一个模块,这种模块可以理解为动态模块,动态模块支持删除
这两者底层实现就是用一个runtime
来判断,runtime
为false
表示静态的模块,反之为动态模块(这个之后会写)
而且通过入参可以看出,注册一个模块,是传入一个path
数组的,比如现在有如下store
1 2 3 const store = createStore ({ state : {} })
现在想在根下面注册一个名为m1
的模块,那么应该执行
1 2 3 4 5 store.registerModule (['m1' ], { state : {}, getters : {}, })
然后想在m1
下面注册一个m2
模块
1 2 3 4 5 store.registerModule (['m1' , 'm2' ], { state : {}, getters : {}, })
unregisterModule
卸载一个动态的模块,对于初始化的模块,是无法卸载的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export class Store { unregisterModule (path) { if (typeof path === 'string' ) path = [path] if (__DEV__) { 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 )) delete parentState[path[path.length - 1 ]] }) resetStore (this ) } }
可以看到,卸载的主要实现也是_modules.unregister
然后通过getNestedState
查找对应的父状态,然后使用delete
操作删除。
最后重置了store
,这个resetStore
包括了resetStoreState
操作,后面会写到
hasModule
判断模块名字是否被注册了
1 2 3 4 5 6 7 8 9 10 11 export class Store { hasModule (path) { if (typeof path === 'string' ) path = [path] if (__DEV__) { assert (Array .isArray (path), `module path must be a string or an Array.` ) } return this ._modules .isRegistered (path) } }
可以看到实现也是依赖_modules.isRegistered
,做了简单的判断
内部函数 installModule
安装模块,递归的安装它的子模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 function installModule (store, rootState, path, module , hot ) { const isRoot = !path.length const namespace = store._modules .getNamespace (path) if (module .namespaced ) { if (store._modulesNamespaceMap [namespace] && __DEV__) { console .error (`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/' )} ` ) } store._modulesNamespaceMap [namespace] = module } if (!isRoot && !hot) { const parentState = getNestedState (rootState, path.slice (0 , -1 )) const moduleName = path[path.length - 1 ] store._withCommit (() => { if (__DEV__) { if (moduleName in parentState) { console .warn ( `[vuex] state field "${moduleName} " was overridden by a module with the same name at "${path.join('.' )} "` ) } } parentState[moduleName] = module .state }) } const local = module .context = makeLocalContext (store, namespace, path) 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) }) }
在前面constructor
中执行了installModule(this, state, [], this._modules.root)
来安装模块
1 2 const isRoot = !path.length const namespace = store._modules .getNamespace (path)
先判断了是否为根模块(isRoot
),以及获取这个模块的命名空间namespace
1 2 3 4 5 6 7 if (module .namespaced ) { if (store._modulesNamespaceMap [namespace] && __DEV__) { console .error (`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/' )} ` ) } store._modulesNamespaceMap [namespace] = module }
如果模块是带命名空间的(注意这里是两个东西namespaced
和namespace
)
那么会注册到_modulesNamespaceMap
这个对象中,比如
1 2 3 4 5 6 7 const store = createStore ({ modules : { m1 : { namespaced : true } } });
那么此时m1
子模块在_modulesNamespaceMap
中对应m1/
属性
接着是一段判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (!isRoot && !hot) { const parentState = getNestedState (rootState, path.slice (0 , -1 )) const moduleName = path[path.length - 1 ] store._withCommit (() => { if (__DEV__) { if (moduleName in parentState) { console .warn ( `[vuex] state field "${moduleName} " was overridden by a module with the same name at "${path.join('.' )} "` ) } } parentState[moduleName] = module .state }) }
这里我们先简单认为hot
就是false
,也就是在不是根模块 的情况下才会设置状态
那为啥根不设置状态呢,其实根的状态在constructor
中由ModuleColleton
对象来设置了
在constructor
中this._modules = new ModuleCollection(options)
初始化了根root
的状态
ModuleCollection
这个之后会写。
可以看到if
内的实现是先通过getNestedState
找到他的父状态对象
然后获取模块的名字moduleName
,做了个判断,防止重名模块的出现,
最后就在父状态下挂载对应子模块的状态parentState[moduleName] = module.state
,比如
1 2 3 4 5 6 7 8 9 10 11 12 const store = createStore ({ state : { val : 1 }, modules : { m1 : { state : { val : 2 } } } })
运行这个函数之后,store.state
就变成了
1 2 3 4 5 6 store.state = { val : 1 , m1 : { val : 2 } }
接着创建了当前模块的上下文参数,用于之后注册getters
,mutations
和actions
1 const local = module .context = makeLocalContext (store, namespace, path)
接着分别遍历了当前模块的getters
,mutations
和actions
调用registerGetter
,registerMutation
和registerAction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) })
最后遍历模块的modules
,递归的处理这个过程
1 2 3 module .forEachChild ((child, key ) => { installModule (store, rootState, path.concat (key), child, hot) })
这里可以总结出这个函数的作用,也就是把每个模块的state
的状态聚合到根模块的的state
上(也就是this._modules.root.state
,在构造器中的语句,传入了这个函数)
注册getters
,mutations
,actions
这里没有发现和响应式相关的语句,因为响应式的处理在另一个函数
注意,在Vuex
中,很多时候用一个path
数组来表示当前模块的state
在递归注册子模块执行installModule
时,传入path.concat(key)
,比如
如果此时模块为root
,那么此时path
数组为[]
如果此时root
包含了一个名字为m1
的模块,那么遍历到m1
模块时,path
数组为['m1']
resetStoreState
重置store
的状态,在这个函数中,会把_state
变成响应式对象
并且对wrappedGetters
中的getter
也转为computed
计算属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 function resetStoreState (store, state, hot ) { const oldState = store._state store.getters = {} store._makeLocalGettersCache = Object .create (null ) const wrappedGetters = store._wrappedGetters const computedObj = {} forEachValue (wrappedGetters, (fn, key ) => { computedObj[key] = partial (fn, store) Object .defineProperty (store.getters , key, { get : () => computed (() => computedObj[key]()).value , enumerable : true }) }) store._state = reactive ({ data : state }) if (store.strict ) { enableStrictMode (store) } if (oldState) { if (hot) { store._withCommit (() => { oldState.data = null }) } } }
先是在store
上初始化getters
和_makeLocalGettersCache
1 2 3 4 store.getters = {} store._makeLocalGettersCache = Object .create (null )
然后遍历了在installModule
中注册的所有的getter
,也就是_wrappedGetters
上的所有属性
1 2 3 4 5 6 7 8 9 10 forEachValue (wrappedGetters, (fn, key ) => { computedObj[key] = partial (fn, store) Object .defineProperty (store.getters , key, { get : () => computed (() => computedObj[key]()).value , enumerable : true }) })
注意这里利用Object.defineProperty
延迟了调用computed
创建一个计算属性
然后就是对state
响应式化,使用Vue3
的reactive
API来创建一个响应式对象
1 2 3 store._state = reactive ({ data : state })
接着根据strict
属性判断是否要开启严格模式,也就是是否执行enableStrictMode
函数
1 2 3 4 if (store.strict ) { enableStrictMode (store) }
最后一个判断和热重载有关,忽略。
总结来说这个函数就是给已经聚合的this._modules.root.state
(在构造函数中,传入了这个函数)对象设为响应式
并把_wrappedGetters
上的getter
设为计算属性,挂载到store.getters
上
resetStore
重置一个store
,清除注册的所有actions
,mutations
,getters
和模块对象
然后根据原来的state
重新执行installModule
和resetStoreState
1 2 3 4 5 6 7 8 9 10 11 12 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 installModule (store, state, [], store._modules .root , true ) resetStoreState (store, state, hot) }
makeLocalContext
可以理解为为每一个模块创建一个上下文,包括这个模块自己的dispatch
和commit
函数
以及挂载getters
和state
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function makeLocalContext (store, namespace, path ) { const noNamespace = namespace === '' const local = { dispatch : noNamespace ? store.dispatch : (_type, _payload, _options ) => { const args = unifyObjectStyle (_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root ) { type = namespace + type if (__DEV__ && !store._actions [type]) { console .error (`[vuex] unknown local action type: ${args.type} , global type: ${type} ` ) return } } return store.dispatch (type, payload) }, commit : noNamespace ? store.commit : (_type, _payload, _options ) => { const args = unifyObjectStyle (_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root ) { type = namespace + type if (__DEV__ && !store._mutations [type]) { console .error (`[vuex] unknown local mutation type: ${args.type} , global type: ${type} ` ) return } } store.commit (type, payload, options) } } Object .defineProperties (local, { getters : { get : noNamespace ? () => store.getters : () => makeLocalGetters (store, namespace) }, state : { get : () => getNestedState (store.state , path) } }) return local }
根据Vuex
的文档,如果一个模块没有设置命名空间namespaced
的话,
那么他的action
和mutation
都会直接注册到根上
也就是不同的模块可以对同一个名字注册action
或者mutation
也就有了之前说的_mutations
对象每个属性对应的不是单个函数,而是一个函数数组,比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const store = createStore ({ mutations : { mutation1 ( ) { } }, modules : { m1 : { mutations : { mutation1 ( ) { } } } } })
那么此时mutation1
就会有两个函数,当commit
的时候,这两个函数都会执行(actions
同理)
而如果模块m1
设置namespaced
为true
的话,那么m1
的mutation1
就会带上命名空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const store = createStore ({ mutations : { mutation1 ( ) { } }, modules : { m1 : { mutations : { mutation1 ( ) { } } } } })
那么此时m1
的mutation1
在根中就会带上m1/
的前缀
可以以dispatch为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dispatch : noNamespace ? store.dispatch : (_type, _payload, _options ) => { const args = unifyObjectStyle (_type, _payload, _options) const { payload, options } = args let { type } = args if (!options || !options.root ) { type = namespace + type if (__DEV__ && !store._actions [type]) { console .error (`[vuex] unknown local action type: ${args.type} , global type: ${type} ` ) return } } return store.dispatch (type, payload) }
存在命名空间的情况下,就是把命名空间添加到对应的action
名字前,然后执行dispatch
,这样就能取到对应的action
而在当前的模块下,并不需要知道真正的action
名字,因为这里已经替我们处理了
比如模块m1
的ac2
分发了ac1
,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const store = createStore ({ actions : { ac1 ( ) { console .log ("root ac1" ); } }, modules : { m1 : { namespaced : true , actions : { ac1 ( ) { console .log ("m1 ac1" ); }, ac2 ({dispatch} ) { dispatch ("ac1" ); } } } } });
除了包装了dispatch
和commit
如果存在命名空间,也会注入相应的getters
和state
1 2 3 4 5 6 7 8 9 10 Object .defineProperties (local, { getters : { get : noNamespace ? () => store.getters : () => makeLocalGetters (store, namespace) }, state : { get : () => getNestedState (store.state , path) } })
如果存在命名空间,那么会执行makeLocalGetters
,根据命名空间来拿到对应的getters
,
而本模块的state
只要通过getNestedState
来从根获取对应嵌套的state
即可
makeLocalGetters
通过命名空间来获取相应模块的getter
,然后对结果缓存,减少计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function makeLocalGetters (store, namespace ) { if (!store._makeLocalGettersCache [namespace]) { const gettersProxy = {} const splitPos = namespace.length Object .keys (store.getters ).forEach (type => { if (type.slice (0 , splitPos) !== namespace) return const localType = type.slice (splitPos) Object .defineProperty (gettersProxy, localType, { get : () => store.getters [type], enumerable : true }) }) store._makeLocalGettersCache [namespace] = gettersProxy } return store._makeLocalGettersCache [namespace] }
先判断_makeLocalGettersCache
是否已经缓存了结果,如果缓存了,直接返回
没有缓存的话,会把根的getters
带相应命名空间的提取出来,然后缓存下来并返回。
比如此时执行了makeLocalGetters(store, "m1/")
也就是要找出m1
的getter
,对根getters
中的每个getter
执行slice(0, namespace.length)
截取getter
名字的前面部分和namespace
比较
相等就加入结果集,然后缓存返回,而且要注意,此时的getter
已经不带命名空间了
1 2 3 4 5 6 7 8 const localType = type.slice (splitPos)Object .defineProperty (gettersProxy, localType, { get : () => store.getters [type], enumerable : true })
registerGetter
把模块的getter
注册到_wrappedGetters
上,
此时根据传入的local
(也就是之前通过makeLocalContext
创建的本模块的上下文)
做了一个函数的包装,延迟执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function registerGetter (store, type, rawGetter, local ) { if (store._wrappedGetters [type]) { if (__DEV__) { console .error (`[vuex] duplicate getter key: ${type} ` ) } return } store._wrappedGetters [type] = function wrappedGetter (store ) { return rawGetter ( local.state , local.getters , store.state , store.getters ) } }
registerMutation
把模块的mutation
注册到_mutations
上
此时根据传入的local
(也就是之前通过makeLocalContext
创建的本模块的上下文)
做了一个函数的包装,延迟执行
1 2 3 4 5 6 7 function registerMutation (store, type, handler, local ) { const entry = store._mutations [type] || (store._mutations [type] = []) entry.push (function wrappedMutationHandler (payload ) { handler.call (store, local.state , payload) }) }
registerAction
把模块的action
注册到_actions
属性上
action
的存储方式和mutation
一样,是以数组方式的,也就是一个action
名字可以对应多个action
的函数
这里也对action
进行包装,使得它统一返回一个Promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function registerAction (store, type, handler, local ) { const entry = store._actions [type] || (store._actions [type] = []) entry.push (function wrappedActionHandler (payload ) { let res = handler.call (store, { dispatch : local.dispatch , commit : local.commit , getters : local.getters , state : local.state , rootGetters : store.getters , rootState : store.state }, payload) if (!isPromise (res)) { res = Promise .resolve (res) } if (store._devtoolHook ) { return res.catch (err => { store._devtoolHook .emit ('vuex:error' , err) throw err }) } else { return res } }) }
getNestedState
1 2 3 function getNestedState (state, path ) { return path.reduce ((state, key ) => state[key], state) }
传入一个state
和path
数组,取得对应路径的状态,比如
1 2 3 4 5 6 7 8 9 10 11 const state = { a : { b : { c : { msg : 'Hello Vuex' } } } } const nestedState = getNestedState (state,['a' , 'b' , 'c' ]);
这时候nestedState
就是{ msg: "Hello Vuex" }
数组的reduce
API每次返回了下一个名字为key
状态state[key]
,不过由于没有做判断,如果中间出现了不存在的状态那么会报错
1 2 3 4 5 getNestedState (state, ['a' , 'b' , 'd' ]); getNestedState (state, ['a' , 'd' , 'e' ]);
后记 第二次写这么长的文了,可能有些地方写的比较晦涩,如果你有更好的建议可以在下面评论
除了store.js
,还有几个文件需要编写,不过需要一点时间
Vuex
小巧而且精致,很多时候我会惊叹里面的写法,希望我以后也能写出这么好的代码
人啊,最重要的就是开心~