伪前端带你不完全解析Vuex源码(一)
Vuex是什么?
大家可以先把github上的 vuex项目 clone下来或者直接下载下来用编辑器打开
git clone https://github.com/vuejs/vuex.git
Vuex项目目录
项目的目录结构很清晰,重点会解析
module-collection.js
module.js
index.js
mixin.js
store.js
这几个js文件。
当我们要使用vuex管理状态的时候就要规划好vuex的结构目录了,对于大型的vue应用官方也有推荐的项目结构:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
结构中store目录下面的index.js
文件就是用来生成一个管理状态store对象
通常我们会在store/index.js
文件中敲入这些代码
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import mucations from './mucations'
import * as actions from './actions'
import * as getters from './getters'
const debug = process.env.NODE_ENV !== 'production'
Vue.use(Vuex)
export default new Vuex.Store({
state,
mucations,
actions,
getters,
strict: debug
})
import Vuex from 'vuex'
就是导入Vuex项目中的入口文件index.js
中的接口
到这边就要对Vuex项目中的源码进行不完全解析,让我们回到Vuex项目中
入口
// index.js
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
这个就是Vuex对外暴露的接口,其中 Store
就是Vuex提供的状态管理类,稍后会进行介绍,再来看看install
,这个函数会在Vue.use(Vuex)
的时候调用。
查看src/store.js
里面的install
export 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)
}
_Vue
就是Vue.use(Vuex)
调用install的时候传入的Vue
引用,在index.js
开头会定义一个局部的Vue引用,用于Vuex中使用
// ...
let Vue // bind on install
// ...
在执行install的时候会判断局部Vue引用是否跟传入的Vue引用是一样的,如果是一样的说明Vuex已经加载了,在非生产环境下会报错,否则让局部Vue也指向传来的Vue,便于Vuex内部使用Vue,函数的最后,又调用了applyMixin(Vue)
,这个函数是实现通过this.$store.xxx
这种方式来访问数据和管理数据,具体怎么实现查看在src/mixin.js
代码
// mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
由于vue1.x和vue2.x的生命周期钩子有不同,所以会进行版本的判断,让vue在初始化(vue2.x是beforeCreate,vue1.x是init)钩子函数数组中mixin一个vuexInit
函数,实现Vue组件初始化的时候执行vueInit
往vue组件中注入$store
这样我们就可以在vue组件中使用 this.$store.xxx
访问到store管理对象中的数据和方法,具体怎么在vue组件中注入$store
属性,查看vuexInit函数中的代码
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
上面这段代码会在Vue组件初始化的时候被调用,他会从构造这个vue实例的options参数中判断是否含有store属性,如果有则把这个store注入到vue实例的$store
属性中,就比如说我们vue应用的整个项目的入口文件是main.js
里面的代码是
// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store/index'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
就main.js
中创建的vue组件其实是最顶层的vue组件<Root></Root>
,这里的if (options.store)
就是判断在new Vue({...})
传给Vue构造函数的参数也就是options中是否有store这个对象,如果有就注入到这个顶层vue实例的$store
的属性中,这时候最顶层的vue组件<Root><App>...</App></Root>
Root中就包含了$store
,因为每个vue组件被创建之前都会调用beforeCreat/Init
生命周期下的所有函数,包括被mixin到里面的vuexInit
所以当App组件被创建的时候,他本身的options参数是没有store
的,而
else if (options.parent && options.parent.$store)
他会去判断是否含有父组件,这里App的父组件就是Root组件了,而Root有$store
属性所以把父组件的这个属性的引用赋值给App这个组件的$store
,这样就达到每个vue组件创建的时候都能拿到store,一级一级的传下去。
看到这里也不是说store只能在最顶层的组件出现,这样是为了在这个项目的所有vue组件都能通过这个状态管理来管理数据,store是可以在任何一个实例化vue组件的options中出现的,只不过这样只能在当前组件和其子孙组件中使用store而已。可以采用Bus
等来实现没必要用Vuex。
到这边就实现了 为什么我们在组件中可以使用this.$store.xxx
来访问数据,管理数据来。
Store类
接下来针对具体怎么实现状态管理来解析代码
回到store.js
中对Store类进行解析,这个是用来实例化一个store对象的也就是this.$store.xxx
中的store
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 #731
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.`)
}
const {
plugins = [],
strict = false
} = options
// store internal state
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._watcherVM = new Vue()
// bind commit and dispatch to self
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)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(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 plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
在构建一个 store
实例之前要确保Vuex已经被加载,所以一开始的
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
这段代码判断没加载就调用install函数加载Vuex原理和上面解析install的时候一样。
下面的代码是用来判断依赖
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.`)
}
里面的assert
是他们自己实现的一个断言函数,代码比较简单,可以查看utils.js
里面的函数实现。
先判断Vue
因为里面用到的比如刚才的往组件中注入$store
属性或者等下要介绍的数据监听等等功能都依赖Vue。还要断言环境是否支持Promise
,在actions中会用到Promise
所以Promise
也必须被支持,最后一个断言是 当前的store
对象,必须是通过Store
类创建的。
const {
plugins = [],
strict = false
} = options
这段代码则是通过ES6的解构赋值,把构造store
store = new Vuex.Store({...})
时候的参数options
,赋值成局部可引用的变量
// store internal state
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._watcherVM = new Vue()
_committing
:用来判断是否是通过commit
来实现更改数据因为commit
是修改state
中数据的唯一合法途径
_actions
:存放把我们store/actions
下面编写的所有action函数通过注册后的函数集合,就是注册后的action
_actionSubscribers
:订阅了action变化的订阅者集合
_mutations
:存放把我们store/mutations
下面编写的所有mutation函数通过注册后的函数集合,就是注册后的mutation
_wrappedGetters
: 对我们编写的getters
函数进行包装后的函数,用于组册getters
_modules
:当我们的项目很庞大的时候,很多数据都要用状态管理,这时候把所有的数据都存在一个store进行管理会显得臃肿而且复杂,变得难以管理,所以Vuex提供了module,也就是每个module下包含各自的state,actions,getters,mutations
。他是根据ModuleCollection
这个类生产一个module树,接下来会对这个类进行解析
_modulesNaspaceMap
:用于存放命名空间下对应的module
_subscribers
:用来存储所有对 mutation 变化的订阅者
_watcherVM
:一个vue实例,利用vue的watch来监测state数据的变化
至于Object.create(null)
是创建一个空对象但是又有别于var obj = {}
中这种通过字面量创建的空对象,因为Object.create(null)
创建的空对象的__proto__
是指向null的,Object.create
创建对象的大概过程就是:
function (o) {
var F = function () {}
F.prototype = o
var f = new F()
return f
}
this._modules = new ModuleCollection(options)
通过把options也就是创建store时候的参数传给ModuleCollection
构建module树
// module-collection.js
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
这个是ModuleCollection的构造器,rawRootModule就是我们前面传将来的options,构造器通过调用register()
函数来注册module构建module树
// module-collection.js
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
比如我们一开始传过来的参数是register([], rawRootModule, false)
第一个参数是用来表示也可以说是层级关系,rawModule
则是用于构造一个module所用的参数,runtime
表示mudule是否是运行时。
我们先分析为根module的情况,也就是rawModule
为构建store时候传进来的option,一开始也是先对传进来的rawModule
先做断言,看是不是符合构造一个module
,这边的代码也是比较好理解的这边就不解析了。
通过 const newModule = new Module(rawModule, runtime)
创建一个module对象
来看看Module代码
// module.js
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
设置这个module的运行时状态为false,因为一开始这个根module的子module还没注册所以为空,所以先this.children = Object.create(null)
赋值个空对象,还有设置这个module的state属性,也就是根module的state。
然后再回到module-collection.js
中,这时候这个 newModule
就是根module了,因为一开始path === []
所以设置这个store的root为根module。接下来会判断这个rawModule
是否配置了子module,如果没有则整个module就一个root module,如果有子module,forEachValue
也是他们自己封装的枚举对象的,具体可以查看 utils.js
他把rawModule.modules
的对象当作构建子module的参数,还有把当前子module的key放到path中递归调用register从而实现module树的构建。假如刚才创建的root
还存在子module的话,第二次调用register,假设rawModule.modules
存在moa: {...}
这样一个module配置的话,那么第二次调用register传进去的参数就是,register(['moa'], moa, runtime)
然后再通过 new Module()
创建一个module对象,然而这时候path.length !== 0
通过const parent = this.get(path.slice(0, -1))
来获取moa这个module的父module
get (path) {
// module-collection.js
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
这段代码就是用来获取当前module的父module,因为要找父module肯定不能包含自己,所以通过path.slice(0, -1)
传到get
函数中的时候就不包含自己了,由于这时候get中path为空数组所以直接返回this.root
就是根module,这时候parent === this.root
为true,然后在这个父module中的_childre
添加当前的newModule,key就是moa,这时候root就有子module了。递归直到没有其他module配置为止,就完成了module树的构建。
现在再回到store.js
这个文件中,
this._modules = new ModuleCollection(options)
这个_modules
就是刚才构建出来的module树了。
// store.js
// bind commit and dispatch to self
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)
}
让store这个局部变量指向store对象,然后对store里面的dispatch和commit绑定当前的store对象。也就是这两个函数里面的this都是指向这个store对象
// store.js
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
利用unifyObjectStyle
这个函数来统一参数,因为我们commit的时候有两种参数方式具体可以查看Vuex的官网。
通过参数构建一个mutation对象,从组册的mutatons中通过type也就是我们定义mutation时候的函数名字,拿到同名的mutation集合entry,为什么说同名呢,下面在安装module的时候会讲到,会先对这entry进行判断如果没有这个mutation则报错,接下来会调用,_withCommit()
这个函数进行state的改变,传进一个遍历执行对于mutation的函数其中payload就是我们定义mutation时候的函数的参数。
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
其实这个函数不复杂,我们在commit的时候去改变_committing
的状态,为true则表示正在通过commit方式改变state,用于在watch监听数据改变的时候判断是否是通过commit来改变的。然后执行我们注册的mutation函数。
回到commit函数this._subscribers.forEach(sub => sub(mutation, this.state))
遍历通知订阅了mutation的订阅者,传入最新的state。
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
this._actionSubscribers.forEach(sub => sub(action, this.state))
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
dispatch一开始跟commit是类似的,不同的是最后return那边,会先判断同名的action有几个,如果是一个就是直接执行entry中的第一个action,如果大于一个,我们都知道我们可以在action中进行异步的操作,所以如果action中有reutrn数据的话,由于异步的关系,你return出来的数据不一定是你想要的结果,所以在组册action的时候他的包装函数会把不是Promise结果处理成Promise,Promise.all()
,用来确保全部的注册的action函数执行,也就是状态都是resolve否则dispatch不会返回正确的值。所以你获取到var d = dispatch(xxx,xxx)
值后可以通过d.then(res => {})
来获取对于同名的action集合return的值就是res是个数组,所以这边就解释了为什么一开始要判断是否支持Promise了,Promise也是属于ES6的内容。
接着回到Store的构造器中
// strict mode
this.strict = strict
const state = this._modules.root.state
如果开启严格模式的话,Vuex就会监听数据的改变是否是通过commit。
然后把root moudle的状态赋值给一个局部变量
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
这边开始安装modules,我们一开始只是构建了module树,但是mutations,actions,getters都还没注册,而且如果含有多个module的话,状态state树也还没构建。通过installModule
函数来完成这些事情。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(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)
})
}
安装module也是从根module开始 也就是从root开始,参数中,store为当前store对象,rootState为root的state,path就是用来记录module层级关系,module当前要安装的module,hot为热更新。
当安装root module的时候,通过path判断是否是根module,获得当前module的命名空间,比如我之前root下面还有一个moa的module,如果这个module的namespaced为true的时候得到的命名空间就是moa/
。如果moa下面还有一个moa1的module,他的namespaced也为true那么这个moa1的命名空间就是moa/moa1/
,所以要把当前module的名称加到命名空间中必须当前module的namespaced = true
才可以,比如说moa他的namespaced 为 false 那么moa的命名空间为空,而且moa1的namespaced为true,那么moa1的命名空间为moa1/
这样moa就没有出现在命名空间中。
因为我们当前是根module所以 namespace 为空,他设置namespaced也没有意义
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
这三行代码是把如果定义了命名空间,就把当前的module放入这个_modulesNamespaceMap
集合中,key为命名空间,value为当前module
接下来因为不是根module所以这个代码等到安装子module的时候解析
const local = module.context = makeLocalContext(store, namespace, path)
这行代码的作用就是用来生成,当前module的上下文对象,生成当前module下的dispatch,commit,getters,state
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
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 (process.env.NODE_ENV !== 'production' && !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 (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 update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
参数store
为当前的store对象, namespace
为当前module的命名空间,path
则是用来在state中找到当前module的state。因为在Vuex中没有设置命名空间的话,actions,mutations,getters
默认都是组册在全局里面的。具体会在讲解注册他们的时候分析。
local
就是为对应module的actions,mucations,getters,state
提供的上下文对象。
不要忘了我们一开始是安装root module所以是没有命名空间的,所以当前module就是store一开始提供的dispatch,而这个dispatch函数默认执行的是全局的action,如果是有命名空间的module下的dispatch,他对store一开始的dispatch又包装了一层,作用就是自动加上命名空间,这样我们在module里面dispatch的时候只要写action的type就可以对应到这个module下的action而不是全局的,再来看看这个有命名空间下的dispatch多了一个_options
参数,这个作用就是实现dispatch('someOtherAction', null, { root: true })
这种方式来分发全局里面的action。判断完后再调用store一开始提供的dispatch来分发actionreturn store.dispatch(type, payload)
因为这时候的type
已经结果判断是否有加上命名空间了。commit
的原理也是类似的。然后再用Object.defineProperties(....)
在local添加经过命名空间判断后的getters
和state
,对于getters的话没有命名空间的话就返回store.getters
有命名空间的话通过命名空间获取当前module下的getters,来看看makeLocalGetters
这个函数
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) return
// extract local getter type
const localType = 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
})
})
return gettersProxy
}
这个函数就是,从store中注册的getters中,因为getters是一个对象,key是命名空间加我们定义的getters的名字,比如moa/moa1/getusername
其中moa/moa1
是命名空间,getusername
是我们定义的getters的名字。所以我们在一个module中调用getters的时候,也不用带上命名空间。
对于state的话调用了getNestedState
来获取当前module的state
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
这个函数就是用来从state树中来找到对于module的state。
回到makeLocalContext
函数中,他最终返回这个module的上下文对象。用于在注册actions,mutations,getters
中用到。
回到installModule
函数中,接下来的三个函数就是用于注册
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)
})
先来说说注册mutation
他在store的_mutations
中注册的名字就是命名空间+我们定义的mutation名字,看下registerMutation
具体怎么注册
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)
})
}
功能很简单,这边我们会知道,原来store中注册的_mutations
的值是以数组存放的,这也是为什么如果你没有定义命名空间的module下面commit_mutations
中注册的同名mutation的时候,其他的在同个命名空间下的mutation一起执行的原因了。回到代码中,我们编写的,mutation会经过一个包装函数包装,其中handler
,就是我们编写的mutation,这里也会知道为什么我们编写mutation的时候,参数中的state就是当前module下定义的state,而payload就是我们commit的时候带的参数。
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
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
}
})
}
在registerAction
中其实也是跟registerCommit
差不多的实现。这里面我们也会看到,为什么我们在编写action的时候,参数中{dispatch,commit,getters,state,rootGetters,rootState}
是怎么来的了,其实cb
很少用到,这里的包装函数也会把你返回的结果包装成Promise,在解析dispatch
的时候也有提到。
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
这里跟注册mutations和actions不同的地方就是他不能重复。其他地方差不多。但是其实这里的getters还没有真正的注册完成,这时候我们this.$store.getters
是没有东西的。这个会在 《不完全解析Vuex源码(二)》中会提到。
再次回到installModule
函数中,最后回去遍历module树中的子module,如果有就继续安装
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
跟安装root module不同的是installModule
中的这几行代码
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
作用就是构建store的state树,先获取到当前module的父state,然后在父的state下面加入当前module的state,key就module名称,value就是当前module的state。这里我们也会看到commit是改变state的唯一合法途径了。
到这边完成了actions,mutations,getters
的注册(getters其实不能说是真正的注册)module树的安装。还有很重要的一部分就是利用Vue的功能实现数据的响应,和监测将在(二)中解析。
答应我一定要在会用Vuex的前提下再去看源码。