You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
引入了 Store 、install 和一些辅助工具函数,将引入的变量组装成一个对象向外暴露。
当我们在项目中引入 import Vuex from 'vuex' 的之后, Vuex 就是这个组装后默认导出的对象了。
当然我们也可以通过解构的方式。
import{Store,install}from'vuex'`
install 方法
来看一下 install 方法,在 src/store.js 。
exportfunctioninstall(_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// vuexInitapplyMixin(Vue)}
exportclassStore{constructor(options={}){// Auto install if it is not done yet and `window` has `Vue`.// To allow users to avoid auto-installation in some cases,// this code should be placed here. See #731if(!Vue&&typeofwindow!=='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(typeofPromise!=='undefined',`vuex requires a Promise polyfill in this browser.`)assert(thisinstanceofStore,`store must be called with the new operator.`)}const{
plugins =[],
strict =false}=options// store internal statethis._committing=falsethis._actions=Object.create(null)this._actionSubscribers=[]this._mutations=Object.create(null)this._wrappedGetters=Object.create(null)this._modules=newModuleCollection(options)this._modulesNamespaceMap=Object.create(null)this._subscribers=[]this._watcherVM=newVue()// bind commit and dispatch to selfconststore=thisconst{ dispatch, commit }=thisthis.dispatch=functionboundDispatch(type,payload){returndispatch.call(store,type,payload)}this.commit=functionboundCommit(type,payload,options){returncommit.call(store,type,payload,options)}// strict modethis.strict=strictconststate=this._modules.root.state// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(this,state,[],this._modules.root)// initialize the store vm, which is responsible for the reactivity// (also registers _wrappedGetters as computed properties)resetStoreVM(this,state)// apply pluginsplugins.forEach(plugin=>plugin(this))if(Vue.config.devtools){devtoolPlugin(this)}}}
// bind commit and dispatch to selfconststore=thisconst{ dispatch, commit }=thisthis.dispatch=functionboundDispatch(type,payload){returndispatch.call(store,type,payload)}this.commit=functionboundCommit(type,payload,options){returncommit.call(store,type,payload,options)}
将解构出的 strict 变量赋值给 this.strict ,会在实例中使用。
// strict modethis.strict=strict
init module
接下来会调用 installModule 安装 modules。
// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(this,state,[],this._modules.root)
// set stateif(!isRoot&&!hot){constparentState=getNestedState(rootState,path.slice(0,-1))constmoduleName=path[path.length-1]store._withCommit(()=>{Vue.set(parentState,moduleName,module.state)})}
非 root module 并且没有 hot 热更新,初始化的时候并没有进入 if 判断,注册子模块的时候才会进入
调用 getNestedState 方法取出父 module 的 state。
/** * make localized dispatch, commit, getters and state * if there is no namespace, just use root ones */functionmakeLocalContext(store,namespace,path){constnoNamespace=namespace===''constlocal={dispatch: noNamespace ? store.dispatch : (_type,_payload,_options)=>{constargs=unifyObjectStyle(_type,_payload,_options)const{ payload, options }=argslet{ type }=argsif(!options||!options.root){type=namespace+typeif(process.env.NODE_ENV!=='production'&&!store._actions[type]){console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)return}}returnstore.dispatch(type,payload)},commit: noNamespace ? store.commit : (_type,_payload,_options)=>{constargs=unifyObjectStyle(_type,_payload,_options)const{ payload, options }=argslet{ type }=argsif(!options||!options.root){type=namespace+typeif(process.env.NODE_ENV!=='production'&&!store._mutations[type]){console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)return}}store.commit(type,payload,options)}}// getters and state object must be gotten lazily// because they will be changed by vm updateObject.defineProperties(local,{getters: {get: noNamespace
? ()=>store.getters
: ()=>makeLocalGetters(store,namespace)},state: {get: ()=>getNestedState(store.state,path)}})returnlocal}
然后通过 Object.defineProperties 劫持 local 对象的 getters、state。
// getters and state object must be gotten lazily// because they will be changed by vm updateObject.defineProperties(local,{getters: {get: noNamespace
? ()=>store.getters
: ()=>makeLocalGetters(store,namespace)},state: {get: ()=>getNestedState(store.state,path)}})
劫持 getters 的时候也是一个三元表达式,没有命名空间就将 local 的 getters 代理到 store.getters 上,有的话就将 local 的 getters 代理到 makeLocalGetters 函数的返回上。
我们来看一下 makeLocalGetters 方法:
functionmakeLocalGetters(store,namespace){constgettersProxy={}constsplitPos=namespace.lengthObject.keys(store.getters).forEach(type=>{// skip if the target getter is not match this namespaceif(type.slice(0,splitPos)!==namespace)return// extract local getter typeconstlocalType=type.slice(splitPos)// Add a port to the getters proxy.// Define as getter property because// we do not want to evaluate the getters in this time.Object.defineProperty(gettersProxy,localType,{get: ()=>store.getters[type],enumerable: true})})returngettersProxy}
// initialize the store vm, which is responsible for the reactivity// (also registers _wrappedGetters as computed properties)resetStoreVM(this,state)
我们接着来看 resetStoreVM 方法:
functionresetStoreVM(store,state,hot){constoldVm=store._vm// bind store public gettersstore.getters={}constwrappedGetters=store._wrappedGettersconstcomputed={}forEachValue(wrappedGetters,(fn,key)=>{// use computed to leverage its lazy-caching mechanismcomputed[key]=()=>fn(store)Object.defineProperty(store.getters,key,{get: ()=>store._vm[key],enumerable: true// for local getters})})// use a Vue instance to store the state tree// suppress warnings just in case the user has added// some funky global mixinsconstsilent=Vue.config.silentVue.config.silent=truestore._vm=newVue({data: {$$state: state},
computed
})Vue.config.silent=silent// enable strict mode for new vmif(store.strict){enableStrictMode(store)}if(oldVm){if(hot){// dispatch changes in all subscribed watchers// to force getter re-evaluation for hot reloading.store._withCommit(()=>{oldVm._data.$$state=null})}Vue.nextTick(()=>oldVm.$destroy())}}
函数开始就取出 store._vm,初始值是 undefind,会在后面用到。
循环 wrappedGetters 处理所有 getter。
// bind store public gettersstore.getters={}constwrappedGetters=store._wrappedGettersconstcomputed={}forEachValue(wrappedGetters,(fn,key)=>{// use computed to leverage its lazy-caching mechanismcomputed[key]=()=>fn(store)Object.defineProperty(store.getters,key,{get: ()=>store._vm[key],enumerable: true// for local getters})})
// use a Vue instance to store the state tree// suppress warnings just in case the user has added// some funky global mixinsconstsilent=Vue.config.silentVue.config.silent=truestore._vm=newVue({data: {$$state: state},
computed
})Vue.config.silent=silent
functionenableStrictMode(store){store._vm.$watch(function(){returnthis._data.$$state},()=>{if(process.env.NODE_ENV!=='production'){assert(store._committing,`do not mutate vuex store state outside mutation handlers.`)}},{deep: true,sync: true})}
if(oldVm){if(hot){// dispatch changes in all subscribed watchers// to force getter re-evaluation for hot reloading.store._withCommit(()=>{oldVm._data.$$state=null})}Vue.nextTick(()=>oldVm.$destroy())}
functionunifyObjectStyle(type,payload,options){if(isObject(type)&&type.type){options=payloadpayload=typetype=type.type}if(process.env.NODE_ENV!=='production'){assert(typeoftype==='string',`expects string as the type, but found ${typeoftype}.`)}return{ type, payload, options }}
接收 commit 的三个参数,判断 type 如果是一个对象,并且有 type 属性,将 options 赋值为 payload,payload 赋值为 type,type 赋值为 type.type。
watch(getter,cb,options){if(process.env.NODE_ENV!=='production'){assert(typeofgetter==='function',`store.watch only accepts a function.`)}returnthis._watcherVM.$watch(()=>getter(this.state,this.getters),cb,options)}
调用 _withCommit 并传入回调函数,在回调函数中会用传入的 state 替换当前 _vm._data.$$state。
registerModule
使用 store.registerModule 方法注册模块:
registerModule(path,rawModule,options={}){if(typeofpath==='string')path=[path]if(process.env.NODE_ENV!=='production'){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)// reset store to update getters...resetStoreVM(this,this.state)}
unregisterModule(path){if(typeofpath==='string')path=[path]if(process.env.NODE_ENV!=='production'){assert(Array.isArray(path),`module path must be a string or an Array.`)}this._modules.unregister(path)this._withCommit(()=>{constparentState=getNestedState(this.state,path.slice(0,-1))Vue.delete(parentState,path[path.length-1])})resetStore(this)}
constfunctionAssert={assert: value=>typeofvalue==='function',expected: 'function'}constobjectAssert={assert: value=>typeofvalue==='function'||(typeofvalue==='object'&&typeofvalue.handler==='function'),expected: 'function or object with "handler" function'}constassertTypes={getters: functionAssert,mutations: functionAssert,actions: objectAssert}functionassertRawModule(path,rawModule){Object.keys(assertTypes).forEach(key=>{if(!rawModule[key])returnconstassertOptions=assertTypes[key]forEachValue(rawModule[key],(value,type)=>{assert(assertOptions.assert(value),makeAssertionMessage(path,key,type,value,assertOptions.expected))})})}functionmakeAssertionMessage(path,key,type,value,expected){letbuf=`${key} should be ${expected} but "${key}.${type}"`if(path.length>0){buf+=` in module "${path.join('.')}"`}buf+=` is ${JSON.stringify(value)}.`returnbuf}
exportdefaultclassModule{constructor(rawModule,runtime){this.runtime=runtime// Store some children itemthis._children=Object.create(null)// Store the origin module object which passed by programmerthis._rawModule=rawModuleconstrawState=rawModule.state// Store the origin module's statethis.state=(typeofrawState==='function' ? rawState() : rawState)||{}}getnamespaced(){return!!this._rawModule.namespaced}addChild(key,module){this._children[key]=module}removeChild(key){deletethis._children[key]}getChild(key){returnthis._children[key]}update(rawModule){this._rawModule.namespaced=rawModule.namespacedif(rawModule.actions){this._rawModule.actions=rawModule.actions}if(rawModule.mutations){this._rawModule.mutations=rawModule.mutations}if(rawModule.getters){this._rawModule.getters=rawModule.getters}}forEachChild(fn){forEachValue(this._children,fn)}forEachGetter(fn){if(this._rawModule.getters){forEachValue(this._rawModule.getters,fn)}}forEachAction(fn){if(this._rawModule.actions){forEachValue(this._rawModule.actions,fn)}}forEachMutation(fn){if(this._rawModule.mutations){forEachValue(this._rawModule.mutations,fn)}}}
/** * Reduce the code which written in Vue.js for getting the state. * @param {String} [namespace] - Module's namespace * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it. * @param {Object} */exportconstmapState=normalizeNamespace((namespace,states)=>{constres={}normalizeMap(states).forEach(({ key, val })=>{res[key]=functionmappedState(){letstate=this.$store.stateletgetters=this.$store.gettersif(namespace){constmodule=getModuleByNamespace(this.$store,'mapState',namespace)if(!module){return}state=module.context.stategetters=module.context.getters}returntypeofval==='function'
? val.call(this,state,getters)
: state[val]}// mark vuex getter for devtoolsres[key].vuex=true})returnres})
mapState 函数是经过 normalizeNamespace 函数处理后返回的函数。
normalizeNamespace
我们来看看 normalizeNamespace 函数:
/** * Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map. * @param {Function} fn * @return {Function} */functionnormalizeNamespace(fn){return(namespace,map)=>{if(typeofnamespace!=='string'){map=namespacenamespace=''}elseif(namespace.charAt(namespace.length-1)!=='/'){namespace+='/'}returnfn(namespace,map)}}
mappedState 最后判断 val 是否是 function,是就调用 call 将 val 的 this 绑定到 Vue 实例,并将 stategetters 作为参数传递,执行后返回,不是 function 根据 key 返回对应的 state。
getModuleByNamespace
getModuleByNamespace 函数主要用来搜索具有命名空间的模块。
/** * Search a special module from store by namespace. if module not exist, print error message. * @param {Object} store * @param {String} helper * @param {String} namespace * @return {Object} */functiongetModuleByNamespace(store,helper,namespace){constmodule=store._modulesNamespaceMap[namespace]if(process.env.NODE_ENV!=='production'&&!module){console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)}returnmodule}
/** * Reduce the code which written in Vue.js for committing the mutation * @param {String} [namespace] - Module's namespace * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function. * @return {Object} */exportconstmapMutations=normalizeNamespace((namespace,mutations)=>{constres={}normalizeMap(mutations).forEach(({ key, val })=>{res[key]=functionmappedMutation(...args){// Get the commit method from storeletcommit=this.$store.commitif(namespace){constmodule=getModuleByNamespace(this.$store,'mapMutations',namespace)if(!module){return}commit=module.context.commit}returntypeofval==='function'
? val.apply(this,[commit].concat(args))
: commit.apply(this.$store,[val].concat(args))}})returnres})
mappedMutation 最后判断 val 是否是 function,是就调用 apply 将 val 的 this 绑定到 Vue 实例,并将 commit 和 args 合并成一个数组作为参数传递,,val 不是 function 就将 commit 调用 apply 改变了 this 指向,将 val 和 args 合并成一个数组作为参数传递,执行后返回。
最后将 res 对象返回。
mapGetters
mapGetters 辅助函数将 store 中的 getter 映射到局部计算属性。
来看一下具体实现:
/** * Reduce the code which written in Vue.js for getting the getters * @param {String} [namespace] - Module's namespace * @param {Object|Array} getters * @return {Object} */exportconstmapGetters=normalizeNamespace((namespace,getters)=>{constres={}normalizeMap(getters).forEach(({ key, val })=>{// thie namespace has been mutate by normalizeNamespaceval=namespace+valres[key]=functionmappedGetter(){if(namespace&&!getModuleByNamespace(this.$store,'mapGetters',namespace)){return}if(process.env.NODE_ENV!=='production'&&!(valinthis.$store.getters)){console.error(`[vuex] unknown getter: ${val}`)return}returnthis.$store.getters[val]}// mark vuex getter for devtoolsres[key].vuex=true})returnres})
我看来看看传入 normalizeNamespace 的回调函数。
首先也是申明 res 空对象,经过 normalizeMap 函数处理后的 getters 调用 forEach 循环处理,在 forEach 的回调函数中, 使用解构取出 key 和 value,每一次循环就以 key 为键、mappedGetter 函数为 value 存入 res 对象,这里会将 val 赋值成 namespace + val,如果有命名空间,此时的 val 应该是类似这样的: cart/cartProducts。
/** * Reduce the code which written in Vue.js for dispatch the action * @param {String} [namespace] - Module's namespace * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function. * @return {Object} */exportconstmapActions=normalizeNamespace((namespace,actions)=>{constres={}normalizeMap(actions).forEach(({ key, val })=>{res[key]=functionmappedAction(...args){// get dispatch function from storeletdispatch=this.$store.dispatchif(namespace){constmodule=getModuleByNamespace(this.$store,'mapActions',namespace)if(!module){return}dispatch=module.context.dispatch}returntypeofval==='function'
? val.apply(this,[dispatch].concat(args))
: dispatch.apply(this.$store,[val].concat(args))}})returnres})
mapActions 处理过程与 mapMutations 函数一模一样,就不在赘述。
createNamespacedHelpers
createNamespacedHelpers 创建基于某个命名空间辅助函数。
来看一下具体实现:
/** * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object * @param {String} namespace * @return {Object} */exportconstcreateNamespacedHelpers=(namespace)=>({mapState: mapState.bind(null,namespace),mapGetters: mapGetters.bind(null,namespace),mapMutations: mapMutations.bind(null,namespace),mapActions: mapActions.bind(null,namespace)})
/** * Get the first item that pass the test * by second argument function * * @param {Array} list * @param {Function} f * @return {*} */exportfunctionfind(list,f){returnlist.filter(f)[0]}
find 接收 list 数组,f 回调函数,调用 filter 返回匹配 f 函数的第一个。
deepCopy
deepCopy 函数:
/** * Deep copy the given object considering circular structure. * This function caches all nested objects and its copies. * If it detects circular structure, use cached copy to avoid infinite loop. * * @param {*} obj * @param {Array<Object>} cache * @return {*} */exportfunctiondeepCopy(obj,cache=[]){// just return if obj is immutable valueif(obj===null||typeofobj!=='object'){returnobj}// if obj is hit, it is in circular structureconsthit=find(cache,c=>c.original===obj)if(hit){returnhit.copy}constcopy=Array.isArray(obj) ? [] : {}// put the copy into cache at first// because we want to refer it in recursive deepCopycache.push({original: obj,
copy
})Object.keys(obj).forEach(key=>{copy[key]=deepCopy(obj[key],cache)})returncopy}
Vuex 是什么?
Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex 的文档 对辅助看源码有不小的帮助,不妨在看源码之前仔细地撸一遍文档。
带着问题去看源码
目录
入口文件
vuex 的入口文件在
src/index.js
引入了
Store
、install
和一些辅助工具函数,将引入的变量组装成一个对象向外暴露。当我们在项目中引入
import Vuex from 'vuex'
的之后,Vuex
就是这个组装后默认导出的对象了。当然我们也可以通过解构的方式。
install 方法
来看一下
install
方法,在src/store.js
。方法首先判断变量
Vue
(store.js
顶部申明的变量) 是否与传入_Vue
全等,如果全等并且在非生产环境,抛出异常。随后将传入的
_Vue
赋值给Vue
,这里主要是为了避免重复安装。然后调用引入的
applyMixin
方法,并将Vue
作为参数传入。applyMixin
在src/mixin.js
作为默认方法导出:取出传入
Vue
的 静态属性version
做不同处理。2.0 采用
mixin
将vuexInit
合并到beforeCreate
生命周期钩子。1.0 重写
_init
方法 将vuexInit
合并到_init
方法中。在
vuexInit
方法中,首先判断如果有options.store
说明是root
节点,并且判断store
是function
就执行将函数返回值赋值给this.$store
,否则options.store
直接赋值。然后判断有父节点,并且父节点有
$store
, 就将父节点的$store
赋值给this.$store
,这样就保证只有一个全局的$store
变量。class Store
我们在使用
Vuex
的时候,会实例化Store
类,并且将一些options
作为参数传入。我们来逐行看一下
Store
构造函数中的constructor
代码。判断
store.js
开始申明的Vue
变量、window
不为undefined
(说明在浏览器环境下)、window
上有Vue
变量、如果全部符合就执行install
方法进行自动安装。这么做主要是为了防止在某些情况下避免自动安装,具体情况请看 issues #731
然后在非生产环境执行,运行一些断言函数。
判断当前
Vue
变量, 在创建store
实例之前必须调用Vue.use(Vuex)
。判断支持
Promise
对象, 因为vuex
的registerAction
时会将不是Promise
的方法包装成Promise
,store
实例的dispatch
方法也使用了Promise.all
,这也是为什么action
支持异步调用的原因。判断
this
必须是Store
的实例。断言函数的实现非常简单。
将传入的
condition
在函数内取非,为true
就抛出异常。接下来是从
options
解构出plugins
strict
。plugins
:vuex
的插件,数组,会在后面循环调用。strict
: 是否是严格模式,后面判断如果是严格模式的会执行enableStrictMode
方法,确保只能通过mutation
操作state
。接下来就是一些初始参数的赋值。
使用
call
将dispatch
commit
的this
绑定到当前的Store
实例上。将解构出的
strict
变量赋值给this.strict
,会在实例中使用。init module
接下来会调用
installModule
安装modules
。第一次调用将
this
、state
(this._modules.root.state
)、空数组、this._modules.root
(root module
)作为参数传入。installModule
代码:首先先根据
path
判断是否是root
,刚开始传入的path
为空数组, 所以是isRoot = true
,随后调用
ModuleCollection
类的getNamespace
方法 根据path
获取命名空间,因为this._modules
是ModuleCollection
类的实例。接着判断
module.namespaced
是否为true
,namespaced
是在每个module
的配置中设置的,如果为true
就将namespace
赋值为key
、module
为值存到construction
的_modulesNamespaceMap
变量上。在
helper.js
我们会用getModuleByNamespace
获取_modulesNamespaceMap
下对应命名空间模块。非
root module
并且没有hot
热更新,初始化的时候并没有进入if
判断,注册子模块的时候才会进入调用
getNestedState
方法取出父module
的state
。path
是一个数组,按模块嵌套排列,path.slice(0, -1)
传入除去自身的数组,就是父级。getNestedState
返回一个三元表达式,如果有path.length
就调用reduce
方法取出对应嵌套的state
,没有返回直接传入的state
。然后调用
store
的_withCommit
方法:_withCommit
中执行传入的fn
之前会将this._committing
置为true
,执行fn
函数后,将committing
回复恢复之前的状态。这里主要是为了保证修改
state
只能通过调用_withCommit
,会调用enableStrictMode
去检测state
是否以预期的方式改变,我们在使用vuex
中,就是通过mutation
去改变state
。调用
makeLocalContext
方法:makeLocalContext
主要用来初始化dispatch
、getter
、commit
、state
,通过defineProperties
劫持getters
、state
。声明
noNamespace
变量判断是否有命名空间,然后创建local
对象,改对象有两个属性dispatch
commit
,它们的值分别是 2 个三元表达式,如果是没有命名空间的,dispatch
就赋值为store.dispatch
,有命名空间就拼上再返回,commit
也是一样的道理。然后通过
Object.defineProperties
劫持local
对象的getters
、state
。劫持
getters
的时候也是一个三元表达式,没有命名空间就将local
的getters
代理到store.getters
上,有的话就将local
的getters
代理到makeLocalGetters
函数的返回上。我们来看一下
makeLocalGetters
方法:makeLocalGetters
接收store
和namespace
作为参数。首先申明
gettersProxy
变量,申明splitPos
变量为命名空间长度,随后遍历store.getters
,匹配命名空间,失败就
return
,成功往下执行。然后取出命名空间后的
getter
、type
,使用defineProperty
为gettersProxy
的localType
添加get
方法,劫持gettersProxy
的localType
的get
返回store
上对应的getter
,简单来说就是做了一个有命名空间情况下的代理。makeLocalContext
函数最后会将local
返回。将
makeLocalContext
返回保存到local
、module.context
。下面就是循环注册
mutation
、action
、getter
。调用
module
类的forEachMutation
、forEachAction
、forEachGetter
,取出对应的mutations
、actions
、getters
和回调函数作为参数。来看看
registerMutation
方法:通过
type
取出store._mutations
上对应的mutation
,没有就穿透赋值为空数组,然后将wrappedMutationHandler
函数push
到entry
数组中,函数的参数也就是mutation
时候的参数。函数中调用
call
将handler
函数this
指向store
, 并将local.state
,payload
作为参数传入,这样_mutations[types]
储存了所有的mutation
。来看看
registerMutation
方法:通过
type
取出store._actions
上对应的action
,没有就穿透赋值为空数组,然后将wrappedActionHandler
函数push
到entry
数组中,函数中使用call
将handler
指向store
,call
的第二个参数是dispatch
、commit
、getters
等包装后的对象,所以我们可以在commit
的第一个参数中解构出需要的属性。payload
也就是额外参数,cb
回调函数倒是不怎么用到。然后通过简易的
isPromise
方法判断res
是否为Promise
,只是简单判断了then
是是否为一个函数。如果不是的话,调用
Promise.resolve(res)
将res
包装成一个Promise
。之后就是根据
_devtoolHook
判断当前浏览器是否有devtoolHook
插件,应该是通过Promise.catch
抛出错误,让devtoolHook
捕获。来看看
registerGetter
方法:开始判断如果有相同
getter
就抛出异常,没有的话就以
type
为key
、wrappedGetter
为value
储存到store._wrappedGetters
对象上,每一个getter
都是一个function
。循环注册
mutation action getter
后,只剩下最后一段代码:调用
Module
类的forEachChild
方法,并且将回调函数传入。forEachChild
方法也调用了forEachValue
遍历_children
的key
循环调用传入的fn
。_children
是在ModuleCollection
类中通过嵌套模块的递归注册建立父子关系的。最后递归调用
installModule
完成所以嵌套模块的安装,到此installModule
方法结束。resetStoreVM
resetStoreVM
主要用来重置Vue
实例,实现响应式的state
computed
。我们接着来看
resetStoreVM
方法:函数开始就取出
store._vm
,初始值是undefind
,会在后面用到。循环
wrappedGetters
处理所有getter
。将
store
的getters
赋值为空对象, 取出保存所有注册getter
的_wrappedGetters
对象,申明computed
对象。接着循环
wrappedGetters
对象,将对应的key
以及fn
保存到computed
,这里的fn
就是注册getter
的wrappedGetter
函数。然后通过
defineProperty
劫持store.getters
的key
,代理到store._vm[key]
。保存
Vue.config.silent
变量,设置Vue.config.silent = true
,取消Vue
所有的日志与警告。然后生成一个新的
Vue
实例,将state
和computed
作为参数传入,恢复Vue.config.silent
。因为将store.getters
的key
代理到store._vm[key]
,所以我们可以通过访问this.$store.getters.key
访问到store._vm[key]
。根据
store.strict
判断是否是严格模式,是的话调用enableStrictMode
方法。enableStrictMode
将store
作为参数,调用store._vm.$watch
方法,也就是Vue
实例的$watch
方法,监测this._data.$$state
的变化,就是生成新的Vue
实例的时候传入的state
,判断不是生产模式,调用断言,如果store._committing
是false
, 抛出异常,所以我们在使用vuex
的时候,只能通过mutation
方式改变store
。oldVm
的注销:如果有
oldVm
, 并且是热更新模式,将oldVm._data.$$state
置为null
,接下来调用
oldVm
的$destroy
方法注销oldVm
实例。插件的调用:
循环传入的
plugin
数组,循环调用,并将this
传入。调用
devtoolPlugin
方法:constructor
的末尾会判断Vue.config.devtools
是否为真,调用devtoolPlugin
方法,并将this
作为参数传入,devtoolPlugin
实现请看插件 devtool
部分。至此
Store
类的constructor
部分结束,我们往下来看看Store
类中的方法。代理
state
:为
state
设置get
,访问Store
实例的state
的时候代理带this._vm._data.$$state
。为
state
设置set
,不能直接修改state
, 非生产环境抛出异常,提示你使用store.replaceState
方法修改state
。commit
修改
Vuex
的store
只能通过mutation
,我们通过commit
调用mutation
。commit
接收 3 个参数,_type
就是mutation
的type
,_payload
就是传入的参数,_options
参数会在下面调用,貌似没什么用处,只是用来判断是否console.warn
。接下来调用
unifyObjectStyle
方法:接收
commit
的三个参数,判断type
如果是一个对象,并且有type
属性,将options
赋值为payload
,payload
赋值为type
,type
赋值为type.type
。因为
vuex
允许对象风格的提交方式:处理成这样的形式:
然后从
unifyObjectStyle
结构出type
、payload
、options
,将包装type
、payload
成一个对象赋值给mutation
变量,申明entry
变量从储存所有mutation
的this._mutations
取出对应type
的mutation
,没有对应mutation
就return
,如果在非生产环境,顺便抛出个异常。接着调用
this._withCommit
方法,并将回调函数传入,这里会循环对应的mutation
,将payload
参数传入并调用handler
函数,需要注意的是mutation
只能是是同步函数。接着循环
_subscribers
:_subscribers
是一个数组,循环调用里面的函数,并将mutation
this.state
传入。最后判断非生产环境,并且
options.silent
为真,就抛出异常,提示Silent option
已经删除,应该是和vue-devtools
有关。dispatch
通过
store.dispatch
方法触发Action
:dispatch
接收2个参数,action type
和_payload
参数。与
commit
一样调用unifyObjectStyle
方法处理对象形式的dispatch
,解构出type
payload
,申明action
对象包装type
payload
,申明entry
变量从this._actions
中取出对应的action
,没有对应action
就return
,如果在非生产环境,顺便抛出个异常。接着循环
_actionSubscribers
:_actionSubscribers
是一个数组,循环调用里面的函数,并将action
this.state
传入。与
commit
不同的是,dispatch
最后会返回一个Promise
,entry
是注册action
时储存wrappedActionHandler
函数的数组,在注册action
时会将其包装成promise
,所以在action
中支持异步操作,这里判断entry
长度,如果是多个调用Promise.all
方法,单个直接取第 0 个调用。subscribe
订阅
store
的mutation
:subscribe
中 调用了genericSubscribe
方法,并将回调和this._subscribers
传入,返回一个函数可以停止订阅。会在每个
mutation
完成后调用,通常用于插件,在plugins
的devtool.js
和logger.js
都使用了。genericSubscribe
genericSubscribe
接收fn
函数和一个subs
数组作为参数,首先判断如果在subs
没有fn
函数,就往subs
数组push
fn
,最后return
一个function
,这个函数会取到当前函数在subs
中的下标,然后使用splice
从subs
中删除,也就是说调用返回的函数可以停止订阅。subscribeAction
订阅
store
的action
。subscribeAction
中 调用了genericSubscribe
方法,并将回调和this._actionSubscribers
传入,返回一个函数可以停止订阅。watch
响应式地侦听
fn
的返回值,当值改变时调用回调函数。判断非生产环境并且
getter
不是一个function
抛出异常,随后会return
一个函数,调用返回的函数可以停止监听,this._watcherVM
在constructor
赋值成了一个Vue
实例,其实就是基于Vue
实例的$watch
方法。replaceState
替换
store
的根状态。调用
_withCommit
并传入回调函数,在回调函数中会用传入的state
替换当前_vm._data.$$state
。registerModule
使用
store.registerModule
方法注册模块:registerModule
方法接收path
路径,rawModule
模块,options
配置作为参数。首先判断
path
如果为字符串,就转成字符串数组,在非生产环境断言,
path
必须为一个数组,path.length
必须大于 0。然后调用
this._modules.register
进行注册模块,installModule
进行模块安装,resetStoreVM
重设Vue
实例。unregisterModule
卸载一个动态模块:
调用
this._modules.unregister
进行模块注销,调用_withCommit
,将回调函数传入。回调函数会调用
getNestedState
方法取出父module
的state
,然后调用Vue.delete
删除对应子模块,resetStore
进行store
的重置,其他部分与registerModule
一致。resetStore
接收
store
和 是否hot
作为参数,将
store
的_actions
、_mutations
、_wrappedGetters
、_modulesNamespaceMap
置为null
。调用
installModule
重新安装模块,调用resetStoreVM
重设Vue
实例。hotUpdate
开发过程中热重载
mutation
、module
、action
和getter
:接收一个新的
newOptions
,调用this._modules.update
更新模块,然后调用resetStore
重置store
。余下的方法基本都在上文讲述过,到此
class Store
结束。class ModuleCollection
在上面初始参数的赋值中
this._modules
就是ModuleCollection
类的实例。如果没有嵌套模块,
this._modules
是这样一个结构。来看看
ModuleCollection:
在
ModuleCollection
类的constructor
中首先会执行类的register
方法,将空数组、rawRootModule
(也就是实例化的时候传入的options
)、false
最为最初参数传入。register
方法会递归调用,实现嵌套模块的收集首先会在非生产环境调用
assertRawModule
函数,对module
进行一些断言判断,判断rawModule
对象是否有getters
mutations
mutations
为key
值,然后根据预置的类型进行断言。随后就是实例化
Module
新建一个newModule
,判断path.length
,0 说明是root
, 将newModule
保存到this.root
上,然后判断rawModule.modules
是否有嵌套modules
。有就调用
forEachValue
将modules
转换成数组,并且循环调用传入的回调函数,回调函数里又递归调用了this.register
,将path
合并子模块的key
, 循环的子模块、runtime
作为参数传入。第二次进入
register
会进入else
判断,调用Module
类的getChild
addChild
, 建立module
的父子关系,如果仍然嵌套模块继续递归调用this.register
。forEachValue
:assertRawModule
上面说过,
assertRawModule
负责对module
进行一些断言判断,判断rawModule
对象是否有getters
、mutations
、mutations
为key
值,然后根据预置的类型进行断言。assertRawModule
循环assertTypes
对象,循环的key
为getters
mutations
actions
,判断传入模块是否有这些属性。接着从
assertTypes
取出对应属性的value
循环
rawModule[key]
对象,如果key
此时就是getters
,那就是遍历当前模块有所的getter
函数,回调函数是一个断言函数,assertOptions
的assert
会返回对属性类型的判断,作为Boolean
传入,makeAssertionMessage
函数只是对断言函数判断的异常的描述。class Module
来看看
Module
类的代码:Module
类的constructor
中会将传入的rawModule
runtime
保存,申明this._children
,主要是存放该模块的子模块,将rawModule.state
取出保存到this.state
上。Module
类提供了很多方法:namespaced
通过双非取值返回一个布尔值
,作为是否有命名空间的判断。addChild
在ModuleCollection
的register
方法中调用,将子模块存入到父模块的this._children
removeChild
删除子模块getChild
获取子模块update
在ModuleCollection
的update
的调用,负责整个模块的更新后面的几个方法都是调用
forEachValue
,将对应对应的模块,以及传入的fn
传入。getNamespace
根据
path
处理命名空间:辅助工具函数
在
vue
的入口文件默认导出辅助工具函数。我们可以通过解构调用
vuex
暴露出来的辅助工具函数。辅助工具函数在
src/helpers.js
:可以看到
helpers.js
向外暴露了 5 个辅助工具函数,在vuex
入口文件中包装成对象后暴露出去。mapState
mapState
辅助函数帮助我们生成计算属性。来看一下具体实现:
mapState
函数是经过normalizeNamespace
函数处理后返回的函数。normalizeNamespace
我们来看看
normalizeNamespace
函数:normalizeNamespace
,接收一个fn
作为参数,最后返回一个函数。此时
mapState
就等于这个函数,它接收namespace
、map
作为参数,namespace
就是命名空间,map
就是传过来的state
。判断
namespace
不是一个字符串,因为mapState
第一个参数是可选的,如果不是字符串就说明没有命名空间,第一个参数就是传入的state
,将namespace
赋值给map
,然后将namespace
置为空字符串。进入else if
判断namespace
最后一个字符串是否是'/'
,没有就拼上'/'
。当调用
mapState
的时候,就会返回fn(namespace, map)
函数的运行后的结果,就是一个res
对象。normalizeNamespace
是一个高阶函数实现,高阶函数是接收一个或者多个函数作为参数,并返回一个新函数的函数。我们来看一下
mapState
中的fn
具体实现。首先申明一个
res
对象,循环赋值后返回,接着调用normalizeMap
函数,normalizeMap
接收一个对象或者数组,转化成一个数组形式,数组元素是包含key
和value
的对象。normalizeMap
经过
normalizeMap
函数处理后,会转化成一个数组,[{key: key, val: fn}]
的格式,调用forEach
循环处理,在forEach
的回调函数中。使用解构取出
key
和value
,每一次循环就以key
为键,mappedState
函数为value
存入res
对象,在
mappedState
函数中,声明state
和getters
变量保存this.$store.state
和this.$store.getters
。接着判断传入的
namespace
,如果有namespace
就调用getModuleByNamespace
函数搜索对应模块,如果没有搜索到就return
,有对应模块的话将对应模块的state
getters
赋值给声明的state
和getters
变量。mappedState
最后判断val
是否是function
,是就调用call
将val
的this
绑定到Vue
实例,并将state
getters
作为参数传递,执行后返回,不是function
根据key
返回对应的state
。getModuleByNamespace
getModuleByNamespace
函数主要用来搜索具有命名空间的模块。函数开始申明
module
变量,然后根据namespace
从store._modulesNamespaceMap
取出对应模块,_modulesNamespaceMap
这个变量是在Store
类中,调用installModule
时候保存所以有命名空间模块的变量。判断非生产环境并且没有对应模块,抛出异常,最后将
module
变量返回。forEach
最后还有一段:应该是
devtools
需要这个属性判断value
是否属于vuex
。完成
forEach
循环后会将处理后的res
对象返回。mapMutations
mapMutations
辅助函数将组件中的methods
映射为store.commit
调用。来看一下具体实现:
mapMutations
处理过程与mapState
相似,我看来看看传入normalizeNamespace
的回调函数。首先也是申明
res
空对象,经过normalizeMap
函数处理后的mutations
调用forEach
循环处理,在forEach
的回调函数中, 使用解构取出key
和value
,每一次循环就以key
为键、mappedMutation
函数为value
存入res
对象, 在mappedMutation
函数中,声明commit
变量保存this.$store.commit
。判断传入的
namespace
,如果有namespace
就调用getModuleByNamespace
函数搜索对应模块,如果没有搜索到就return
,有对应模块的话对应模块的将commit
赋值给声明的commit
变量。mappedMutation
最后判断val
是否是function
,是就调用apply
将val
的this
绑定到Vue
实例,并将commit
和args
合并成一个数组作为参数传递,,val
不是function
就将commit
调用apply
改变了this
指向,将val
和args
合并成一个数组作为参数传递,执行后返回。最后将
res
对象返回。mapGetters
mapGetters
辅助函数将store
中的getter
映射到局部计算属性。来看一下具体实现:
我看来看看传入
normalizeNamespace
的回调函数。首先也是申明
res
空对象,经过normalizeMap
函数处理后的getters
调用forEach
循环处理,在forEach
的回调函数中, 使用解构取出key
和value
,每一次循环就以key
为键、mappedGetter
函数为value
存入res
对象,这里会将val
赋值成namespace + val
,如果有命名空间,此时的val
应该是类似这样的:cart/cartProducts
。在
mappedGetter
函数中,首先判断如果有namespace
并且调用getModuleByNamespace
函数没有匹配到对应模块就直接return
。然后判断在非生产环境并且
this.$store.getters
没有对应的val
就抛出异常并返回。接下来就是有对应模块的情况,直接返回this.$store.getters
对应的getter
。最后将
res
对象返回。mapActions
mapActions
辅助函数将组件的methods
映射为store.dispatch
调用。来看一下具体实现:
mapActions
处理过程与mapMutations
函数一模一样,就不在赘述。createNamespacedHelpers
createNamespacedHelpers
创建基于某个命名空间辅助函数。来看一下具体实现:
createNamespacedHelpers
函数接受一个字符串作为参数,返回一个包含mapState
、mapGetters
、mapActions
和mapMutations
的对象。以
mapState
为例,调用mapState
函数的bind
方法,将null
作为第一个参数传入,不会改变this
指向,namespace
作为第二个参数。此时的
mapState
函数就是经过bind
处理过的,会将namespace
作为第一个参数传入。相当于下面这样:
简化了重复写入命名空间。
到此
helpers.js
结束。工具函数
工具函数在
src/util.js
。find
find
接收list
数组,f
回调函数,调用filter
返回匹配f
函数的第一个。deepCopy
deepCopy
函数:deepCopy
接收一个obj
和cache
数组作为参数,初次调用时cache
为空数组。首先判断
obj
全等于null
或者obj
的类型不等于object
就返回obj
,接下来调用find
,将cache
和 回调传入,会使用filter
去过滤匹配的对象,c.original
全等于当前循环的obj
对象 ,这里判断的是一个引用地址,find
函数会返回匹配f
函数的第一个。如果有
hit
就说明是环形结构,直接返回hit.copy
。所谓环形环形结构,就是对象之间相互引用。
接下来申明
copy
变量,如果obj
是数组copy
等于空数组,否则就是空对象,保存
cache
:以
original
为key
,obj
为value
,将已经上面申明的copy
变量包装成对象push
到cache
数组中。循环
obj keys
,递归调用deepCopy
将obj[key]
和缓存的cache
作为参数传入。最后将深拷贝的
copy
对象返回。forEachValue
forEachValue
接收obj
和fn
作为参数,使用
Object.keys()
将obj
转化成数组,使用forEach
循环调用,在
forEach
的回调函数中,会将obj[key]
key
作为参数传入fn
,循环调用fn
函数。isObject
isObject
接收obj
作为参数,返回obj
不等于null
并且obj
的类型是object
,判断传入的对象是否是纯对象,返回Boolean
。isPromise
isPromise
接收val
作为参数,返回有val
并且val
的then
是一个function
,只是简单判断一个有没有then
方法。assert
assert
接收condition
和msg
作为参数,如果condition
取非为真,就调用throw new Error
抛出异常。插件
devtool
根据
window
上的__VUE_DEVTOOLS_GLOBAL_HOOK_
变量判断当前浏览器是否安装了vueTools
,接着来看
devtoolPlugin
函数,devtoolPlugin
函数使用export default
默认导出,在
Store
实例的constructor
中调用。进入
devtoolPlugin
函数内部,接收store
参数,store
调用时候传入的this
,也就是Store
实例,判断没有
devtoolHook
直接retrun
,将devtoolHook
赋值给store._devtoolHook
,会在Store
实例的registerAction
中用到。向
vueTools
emit
vuex:init
事件,并将store
传入,devtoolHook
监听到会根据store
初始化vuex
。devtoolHook
调用on
方法监听vuex:travel-to-state
,监听到就调用回调函数,回调函数里会调用Store
类的replaceState
方法。replaceState
替换当前_vm._data.$$state
。最后调用
Store
类的subscribe
订阅,每一次mutation
改变state
,都会调用devtoolHook
的emit
方法通知devtool
改变mutation
state
。devtoolHook
原理 ?占坑: 猜测是一个
Vue Bus
。createLogger
vuex
有个内置的插件createLogger
,位于src/plugins/logger.js
:createLogger
接收一个options
对象,默认为{}
:createLogger
返回了一个函数,首先申明prevState
变量,赋值为深拷贝后的store.state
对象,调用
store
的subscribe
方法添加事件订阅,传入一个回调函数,在回调函数中接收mutation
state
两个参数,判断logger
的类型为undefined
就return
。申明
nextState
变量,赋值为深拷贝后的回调函数中传入的state
对象,接着判断
filter
函数,这个默认为true
,进入if
循环后会申明time
变量保存当前事件戳,申明formattedTime
变量保存格式化后的时间, 申明formattedMutation
保存处理后的经过mutationTransformer
处理后的mutation
,申明message
保存默认信息,申明startMessage
变量,根据传入的collapsed
赋值为不同的打印方法。接着使用
call
将startMessage
的this
绑定到logger
上,并且传入message
默认参数。接着就是调用
logger.log
打印,随后调用groupEnd
结束当前的分组。最后将
prevState
赋值为nextState
,保持状态更新。两个处理时间的函数:
问题总结
global eventBus 有何缺陷
eventBus
比较适合简单应用,但是随着需求增加,组件之间通信增多,eventBus
就显得不够直观,不方便我们管理,而且随着组件复用的增多,多个组件通信,又相互通信,就容易导致混乱。$store 如何注入到所有子组件
$store
是在 vuex install 初始化的时候赋值的,来看一下代码:在
vuexInit
方法中,首先判断如果有this.$options.store
说明是root
节点,判断store
如果是function
就将函数执行后的返回赋值给this.$store
,否则将options.store
直接赋值给this.$store
。不是
root
节点就从父组件中获取$store
,这样就保证只有一个全局的$store
。mapState 实现
mapState
请看src/helpers.js
的mapState
部分。mapGetter 如何映射
mapGetter
方法最后会返回一个对象,这个对象的每一个key
值是mappedGetter
方法,mappedGetter
会返回this.$store.getters[key]
。Mutation 同步 && Action 异步
在注册
action
时储会将action
的回调包装成promise
,通过dispatch
方法触发action
的时候,最后return
的是个Promise
对象,所以action
支持异步。注册
mutation
和通过commit
方法触发mutation
的时候,都只是一个同步的代码,仍然是同步代码。dispatch 方法实现
dispatch
请看src/store.js
的dispatch
部分。module 分割实现 && 局部状态 namespaced
实例化
ModuleCollection
请看
class ModuleCollection
。如何调用 vue-devtools
在
devtoolPlugin
方法中,取出挂在window
对象的__VUE_DEVTOOLS_GLOBAL_HOOK__
保存到devtoolHook
,通过emit
vuex:init
初始化store
:内置 logger 插件实现
请看插件
devtool
部分。hotUpdate
使用
webpack
的Hot Module Replacement API
实现热重载。时间穿梭功能实现
当我们调用
devtoolHook
方法的时候,会调用devtoolHook
的on
方法监听vuex:travel-to-state
事件。在
vue-devtools
的源码的src/bridge.js
中:我们看到事件监听是通过
Node
的EventEmitter
监听的。在回调函数中接收
targetState
参数,调用Store
的replaceState
方法去修改this._vm._data.$$state
,当我们点击devtoolHook
的某一条mutation
历史记录,就能穿梭到历史记录。但是这个历史记录又是怎么出现的呢?是通过调用
store.subscribe
方法:每当调用
commit
方法的时候,都会调用循环调用
_subscribers
中的回调函数,回调函数会调用devtoolHook.emit
方法,发送vuex:mutation
,说明改变了mutation
,并把mutation
和state
作为参数传入,devtoolHook
就会储存mutation
的历史记录了。vuex
相关在vue-devtools/src/backend/vuex.js
:看到是通过一个
mutations
数组模拟这个历史记录,每次监听到vuex:mutation
事件就是push
mutation
相关。The text was updated successfully, but these errors were encountered: