逐行级源码分析系列(一) Vuex源码
前言
首先这篇文章也是本人第一次发这种技术文章,错别字,分析错误,不知道的东西在所难免,希望大家指正,目前本人还是一位即将大四的学生,写这个系列的目的也是为了记录在源码中学习,通过写博客让我更加熟悉了源码及其内部完完整整的实现,通过这篇文章也让我对vuex的源码变得非常熟悉,在写这完篇文章之前,因为时间原因,断断续续写了两个星期,虽然已经看完了,但是要全部分析完并写出来,太耗费精力和时间。然后这个系列我打算按照这个顺序来写,我会坚持写下来。排版可能有点差,我会慢慢学习,如果里面有错误,大佬轻喷。。
- 逐行级源码分析系列(一) Vuex源码
- 逐行级源码分析系列(二) Redux和React-Redux源码
- 逐行级源码分析系列(三) Vue-Router源码
- 逐行级源码分析系列(四) React-Router-Dom源码
- 逐行级源码分析系列(五) Express源码
- 逐行级源码分析系列(六) Koa核心源码
- 逐行级源码分析系列(七) Typescript版Axios源码
install
当我们使用Vue.use会调用vuex的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 === Vue,则说明已经安装过了就直接返回,不做处理。然后调用了applyMixin(Vue)方法,我们来看下applyMixin方法实现
applyMixin
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 混入beforeCreate,vuexInit方法
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
// 重写_init方法,把vuexInit方法,挂载到options中
Vue.prototype._init = function (options = {}) {
// 这里做了兼容处理,如果有其他库也使用了init方法,就把vuexInit添加到Init数组中
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
// 这个方法的作用就是可以让每个组件都能通过this.$store放问到store对象
function vuexInit () {
// 获取mergeoptios选线
const options = this.$options
// 如果存在store属性
if (options.store) {
// 如果store是一个方法,就调用store,否则直接使用
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 获取父亲的$store属性
this.$store = options.parent.$store
}
}
}
其实整个函数看起来似乎有点复杂
Vue.mixin({ beforeCreate: vuexInit }) 其实只是调用了这段代码,因为这是vue2.0版本及以上才有的方法,我们这里只讨论vue2.0的情况,关于mixin的用法,这里不做介绍,它为所有的组件添加beforeCreate生命周期钩子
下面我们看一下vuexInit方法的实现
// 这个方法的作用就是可以让每个组件都能通过this.$store放问到store对象
function vuexInit () {
// 获取mergeoptions的选项
const options = this.$options
// 这段if逻辑其实实在根组件中,添加了一个store属性,并赋给this.$store
if (options.store) {
// 如果store是一个方法,就调用store,否则直接使用
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 获取父亲的$store属性
this.$store = options.parent.$store
}
} 首先,获取了`this.store
,通过这种方式,能够在组件之间形成一种链式查找,其实本质上是引用了,根组件中的store`,举个例子
new Vue({
router,
store, // $store实际最终指向的都是这里的store
render: h => h(App)
}).$mount('#app') new Vuex.Store(options)
安装install完成之后,我们来看看new Vuex.Store(options)发生了什么,由于源码太多,就只截取构造函数中的代码,一起来看,vuex进行了哪些初始化操作
constructor (options = {}) {
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 //使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
} = options
this._committing = false // 正在提交
this._actions = Object.create(null) // actions对象
this._actionSubscribers = [] // actions订阅数组
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options) // 收集modules,
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
// 根module的state属性
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)
}
}
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 {
//一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)
plugins = [],
strict = false //使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
} = options 上面这段代码,获取了我们传入的配置plugins和strict,上面代码中标注有每个属性的作用,关于详细的使用可以到官网查看,以后会有讲解
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() 这些代码做了一些属性的初始化,我们暂且不看具体是干什么用的,关键是下面这段代码
this._modules = new ModuleCollection(options)
看到这段代码,我们肯定能立马想到,我们传入的modules配置,我们来看看modules做了哪些初始化
new ModuleCollection(options)
constructor (rawRootModule) {
this.register([], rawRootModule, false)
} 这个类的构造函数只有简简单单的一行代码,它的参数rawRootModule,是我们给Vuex.Store(options)传入的完整的options,接下来看看register方法做了什么
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
// 创建Module对象,初始runtime为false
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 如果path = ['user', 'login'], path.slice(0, -1) = ['user'] 会去掉最后一个
// parent是根模块
const parent = this.get(path.slice(0, -1))
// 把模块添加到根Module对象的_children对象中,形式如下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
}
// 如果options中存在modules属性
if (rawModule.modules) {
// 遍历modules都西昂
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 获取每个module对应的options
/*{
modules: {
user: {
state,
mutations
},
login
},
state: {
},
mutations: {
}
}*/
// 看到上面的形式,如果modules里有options,继续递归遍历,
// path = ['user', 'login']
this.register(path.concat(key), rawChildModule, runtime)
})
}
} const newModule = new Module(rawModule, runtime)
代码一上来就创建了一个Module对象,并把options作为参数传入,我们继续看看Module这个类中做了哪些操作
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
// 获取state
const rawState = rawModule.state
// 如果state是个方法就调用
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// ...其他方法
}
上面的构造函数进行了一些初始化,this.runtime记录了是否是运行时,this._children初始化为空对象,它主要是用来,保存当前模块的子模块,this._rawModule记录了,当前模块的配置,然后又对state进行了些处理。然后我们大概知道了new Module做了什么
- 创建了一个_children对象
- _rawModule记录模块配置
其他并不是很重要,我们先不提,再回到new ModuleCollection(options),构造函数中
const newModule = new Module(rawModule, runtime)
这里拿到了Module对象
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 如果path = ['user', 'login'], path.slice(0, -1) = ['user'] 会去掉最后一个
// parent是根模块
const parent = this.get(path.slice(0, -1))
// 把模块添加到根Module对象的_children对象中,形式如下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
} 这是一段逻辑判断,而这个path是在ModuleCollection构造函数中,传入的,初始时为空
this.register([], rawRootModule, false)
/**
*
* @param {*} path 初始为空数组
* @param {*} rawModule options
* @param {*} runtime 初始为false
*/
register (path, rawModule, runtime = true) {...}
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {...} 他把ModuleCollection对象的root属性设置为一个Module对象,也就是代表根module,而else中的逻辑我们暂时不看,因为后面会有递归,下个周期时会进入else分支
// 如果options中存在modules属性
if (rawModule.modules) {
// 遍历modules
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 获取每个module对应的options
/*{
modules: {
user: {
state,
mutations
},
login
},
state: {
},
mutations: {
}
}*/
// 看到上面的形式,如果modules里有options,继续递归遍历,
// path = ['user', 'login']
this.register(path.concat(key), rawChildModule, runtime)
})
} 这段代码,拿到了当前模块的配置,注意:根模块的配置其实就是options, 然后判断是否存在modules,如果存在,就遍历每个模块,这个forEachValue方法,其实实现非常简单,感兴趣的可以去看一下,最终回调函数遍历到每个module,并获取到module对象和它的模块对象的key,也就是模块名。
之后再次调用了下register方法,递归执行
this.register(path.concat(key), rawChildModule, runtime)
注意:path.concat(key), path本来是空数组,在每次递归时都会拼接模块的名字,这段代码非常关键,后面的namespace会有用到
然后我们再次回到register方法的开始
// 创建Module对象,初始runtime为false
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// this.root = new Module(rawModule, runtime)
this.root = newModule
} else {
// 如果path = ['user', 'login'], path.slice(0, -1) = ['user'] 会去掉最后一个
// parent是根模块
const parent = this.get(path.slice(0, -1))
// 把模块添加到根Module对象的_children对象中,形式如下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
} 依然是创建了Module对象,此时的Module已经是子Module了, if-else判断也会执行到else中
if (path.length === 0) {
//...
} else {
// 如果path = ['user', 'login'], path.slice(0, -1) = ['user'] 会去掉最后一个
// parent是根模块
const parent = this.get(path.slice(0, -1))
// 把模块添加到根Module对象的_children对象中,形式如下
// _children = {
// user: new Module(user, runtime)
// }
parent.addChild(path[path.length - 1], newModule)
} 假如我们有两个module,它会获取到除了最后一个的所有module的key列表,并调用get方法
get (path) {
return path.reduce((module, key) => {
// 获取子模块
return module.getChild(key)
}, this.root)
} 这段是get方法的实现,它其实是返回path对应模块的子模块
parent.addChild(path[path.length - 1], newModule)
从最后,把模块添加到,当前模块的_children对象中
addChild (key, module) {
this._children[key] = module
} 最后,通过ModuleCollection对象的root,就可以拿到Module对象树
类似这样
new Vuex.Store({
modules:{
user: {
modules:{
login
}
},
cart: {
}
}
})
// 模拟一下
ModuleCollection = {
root = 根Module: {
_children: {
子module(user): {
_children: {
子module(login)
}
},
子module(cart)
}
}
} 小总结:new ModuleCollection(options)在root这个属性上挂载了一个由module对象组成的树
我们回到new Vuex.Store(options)时的构造函数
this._modules = new ModuleCollection(options)
this._modules拿到了模块的集合
// 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)
} 这段代码,重写了dispatch和commit方法,其实相当于调用了bind方法,我个人认为也可以改写成这样
this.dispatch = this.dispatch.bind(store, type, payload) this.commit = this.commit.bind(store, type, payload)
继续后面的步骤
this.strict = strict
strict使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误
// 根module的state属性
const state = this._modules.root.state 保存根模块的state属性
installModule(this, state, [], this._modules.root)
这段代码虽然简短,但是非常重要,我们来具体分析installModule方法
installModule
/**
*
* @param {*} store store对象
* @param {*} rootState 根module的state对象
* @param {*} path 初始为空数组
* @param {*} module 根module对象
* @param {*} hot
*/
function installModule (store, rootState, path, module, hot) {
} 它的参数如上
// 如果是空数组,说明是根module const isRoot = !path.length
判断是否是根模块
// 返回由module名字 拼接成的字符串 const namespace = store._modules.getNamespace(path)
这段代码很有意思,我们来看下getNamespace方法,它在ModuleCollection类中
getNamespace (path) {
// 根module
let module = this.root
return path.reduce((namespace, key) => {
// 获取子module
module = module.getChild(key)
// 如果模块的namespace存在, 举个列子: 一层模块 user/, 二层模块: user/login/
return namespace + (module.namespaced ? key + '/' : '')
}, '')
} 直接做一个简单的例子,如果我们在每个模块中使用了namespaced,设置为true,当我们调用commit,dispatch等方法时,我们需要这样做
this.$store.dispatch('count/increment')
this.$store.commit('count/INCREMENT') getNamespace要做的其实就是获取到count/increment前面的count/,并返回
// 如果namespaced存在
if (module.namespaced) {
// 初始时store._modulesNamespaceMap[namespace]是不存在的
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
// namespace对应module
store._modulesNamespaceMap[namespace] = module
} 这段代码做的事情,就是把namespace和module作为key,value保存在store对象的_modulesNamespaceMap属性上,关于这个属性在什么地方用,可以参考helper.js的getModuleByNamespace方法,这个方法是实现mapActions,mapMutations的关键,以后也会讲到
然后是这段代码
// 如果不是根root module ,初始时hot也不存在, 初始时hot为ture,所以不会执行下面的
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)
})
} isRoot想必不用多说,就是判断是否是根模块,而hot这个变量又是哪里来的呢,他是installModule方法传入的一个参数,初始时他是空的,但这又有什么用处呢;emmm,由于我自己很少用到,我就不多做详细介绍了(因为菜,所以没用过),具体用法官方文档有详细介绍
我们继续,前面说到,hot是不存在的,而当前又是根节点,所以也不会执行这个if逻辑,但是我们还是要讲一下,不然一会还要回来讲,首先看一下getNestedState方法实现
const parentState = getNestedState(rootState, path.slice(0, -1))
// 具体实现
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
首先它的第一个参数是state,也就是当前模块的state,注意不一定是rootState,不要被调用参数误解,实际上是递归引用的传递,这个函数就是判断当前path是否为空,如果为空,表示它是根模块的state,不为空表示为子模块的state,要注意的是path.slice(0, -1),它获取了除了本身模块名之前的模块名数组,getNestedState函数直接来说就是用来获取父模块的state,从字面意思也可以理解,至于reduce的一些操作就不详细讲解了。
const moduleName = path[path.length - 1]
然后就是获取了当前模块名,接下来关键来了
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
}) 从字面意思,好像是跟随commit调用?没错就是这样。。
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
// 重新设置之前的提交状态
this._committing = committing
} 它就简单的调用了传入的回调函数,设置了前后的状态,然后来看下回调函数的内部
parentState:父模块的state moduleName:当前模块名 module.state:当前模块的state Vue.set(parentState, moduleName, module.state)
关于Vue.set方法的介绍:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性
也就是说,它可以在把每个state属性变为响应式,在commit之前,为什么在之前呢,因为这是初始化阶段,我们没有主动调用commit
我们继续后面的代码
// 重写了dispatch, commit ,getter,state等方法,全部挂载到了当前模块的context属性上 const local = module.context = makeLocalContext(store, namespace, path)
下面我将详细讲解makeLocalContext方法
makeLocalContext
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
// 如果不存在namespace,就重写dispatch方法
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
// 使用namespace拼接action的类型
type = namespace + type
// 如果不使用 namespace/action的形式调用action就会报错
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
} 这面代码返回了一个local对象,并且这些对象对dispatch,commit等方法还有state,getter进行了包装
const noNamespace = namespace === ''
这段代码用来判断是否存在命名空间namespace,然后我们再来看下dispatch
1.dispatch
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
// 使用namespace拼接action的类型
type = namespace + type
// 如果不使用 namespace/action的形式调用action就会报错
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)
}, 首先判断是否有命名空间,如果没有就是正常的dispatch,如果存在,则先统一对象风格unifyObjectStyle
先来看下unifyObjectStyle实现,具体讲解就写在注释里了
// 统一对象风格
function unifyObjectStyle (type, payload, options) {
//
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (process.env.NODE_ENV !== 'production') {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
} 在看这段代码之前,先说一下,一般来说我们都是这样使用dispatch
store.dispatch('incrementAsync', {
amount: 10
}) 但其实也可以这样,并且官方文档也有例子
store.dispatch({
type: 'incrementAsync',
amount: 10
}) 知道这些我们就继续往下分析
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
} 这里是对参数进行了简单的处理,统一处理成了我们平常使用的模式,最后返回了相应的type, payload, options
接下来,回到makeLocalContext方法
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
// 使用namespace拼接action的类型
type = namespace + type
// 如果不使用 namespace/action的形式调用action就会报错
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)
}, 统一这些参数以后,又是一个if判断,第三个参数用的也很少,但是官方文档是有说明的,options 里可以有 root: true,它允许在命名空间模块里提交根的 mutation或action,然后返回了调用store.dispatch方法的返回值,然后我们来看看包装后的commit
2.commit
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)
} 这段代码和dispatch的实现非常相似,就不讲解了,所做的事情就是对参数进行统一
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
}) 然后这段代码是把state和getter代理到了local对象上,
3.getter
判断当前模块是否有命名空间,如果不是,就不做任何处理,否则调用makeLocalGetters方法,并传入store对象和namespace完整模块字符串,至于这个namespace是什么,可以往前翻一翻,有具体的讲解。比如user/login,表示user模块下的login模块的namespace。然后我们来看看makeLocalGetters做了什么
function makeLocalGetters (store, namespace) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// 截取getter中的namespace,如果不相等,就不做处理
if (type.slice(0, splitPos) !== namespace) return
// 获取getter 的namespace后面的字符串
const localType = type.slice(splitPos)
Object.defineProperty(gettersProxy, localType, {
// 把getters中的属性方法,代理到新的对象中
get: () => store.getters[type],
enumerable: true
})
})
return gettersProxy
} 这个函数被调用说明一定是有namespace的,然后遍历getter,此时的getter的属性名是包含有namespace的,至于为什么会有,这个在以后的registerGetters中会有讲解。然后获取到namespace后面真实的getter属性名,并被代理到一个新的对象中,并且被获取时,仍然是使用了完整的namespace,举个例子
假设模块: user/todo store.getters.doSomething() 等价于 store.getters['user/todo/doSomething']()
看完这些相信大家都明白了
4.state
调用了getNestedState方法,这个方法想必不用多说,前面也有讲过,用来获取模块的父模块state,并返回
我们再回到一开始,调用makeLocalContext的位置, 返回的local对象,最终放在了模块的context属性上
const local = module.context = makeLocalContext(store, namespace, path)
接下来我们继续分析,后面的内容
registerMutation
// 遍历mutations
module.forEachMutation((mutation, key) => {
// 把namespace和mutation名进行拼接
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
}) 这段代码,简单来说就是遍历了,当前模块的所有mutations,并对每个mutation调用了registerMutation方法,传入了store对象,完整的namespace + commit名,mutation函数,以及local对象,接下来看看registerMutation方法实现,至于forEachMutation方法,大家可以自己看一下,实现也很简单
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
// 调用mutation, 并传入state和参数
handler.call(store, local.state, payload)
})
} 这个函数,实际上是把当前模块的mutation放在了一个_mutations对象中,那这个属性在哪定义的呢
this._mutations = Object.create(null)
实际上在Store类的构造函数的时候已经初始化为了一个空对象,registerMutation所做的事情,就是把mutations和namespaceType,形成一个映射关系,并且mutations是一个数组,比如这样
{
'user/todo/INCREMENT': [
function() {...}
]
}这里之所以用数组的形式存储函数,我觉得是为了防止重复定义mutation,因为调用之后只有最后一个会生效
entry.push(function wrappedMutationHandler (payload) {
// 调用mutation, 并传入state和参数
handler.call(store, local.state, payload)
}) 然后就是把mutation的调用放在一个函数中,传入了state,payload,在真正调用commit的时候才会循环调用,真实的mutation
下面我们继续看后面的代码
registerAction
module.forEachAction((action, key) => {
// namespace + type
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
}) 这里和前面的处理差不多,只是有个判断,如果action存在root说明是根模块,所以直接用key就好了,options 里可以有 root: true,它允许在命名空间模块里提交根的 mutation,否则就使用namespace和key拼接成的action名,然后我们来看registerAction是实现
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)
}
// 这是给devTool用的,可以不用关心
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
我们暂且不看wrappedActionHandler函数里面的内容,它的处理依旧和mutation的处理一样,也是把action放在_actions对象中,然后再看wrappedActionHandler里的内容,它调用了action,并且让他this指向了store,传入了,local对象中的dispatch,commit等方法还有state,getter,这不就是我们之前看到的,经过处理后的API方法吗。
然后它拿到action调用之后的返回值,最终返回了一个Promise.resolve(res),也就是一个Promise
通过上面这些代码,我们能在实际中这么用
注意:commit, dispatch,getters,state都是当前模块里的方法和对象
{
actions: {
async increment({ commit, dispatch, getters,state, rootGetters, rootState }) {
return await getData()
}
}
} 说完了registerAction,我们来说一说registerGetter
registerGetter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
}) 不多废话,直接看registerGetter的实现
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
)
}
} 一上来就是一个判断,简单点来说就是,不允许有重复定义的getters,我们之前是看到actions和mutation是可以重复定义的。然后再来看其他的,它和之前的处理有所不同,但也相差不大,因为不允许有重复,所以就不需要push一个函数了,直接调用了getter方法,传入了state,getters,根state,根getters,我们可以这样用
{
['INCREMENT']: function(state, getters, rootState, rootGetters){
//...
}
} 讲完这些installModule基本上要结束了,我们看最后一段代码
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
}) 没错,是个递归,它拿到了子模块进行了递归,大家可以翻到前面梳理一下流程
installModule方法我们也讲完了,我们要回到Store类的构造函数中,看看还有些什么初始化操作
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
} 接下来分析resetStoreVM
resetStoreVM
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(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 mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (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是什么,如果有注意到这个函数中间的一段代码的话可以看到,_vm是又创建了一个Vue实例,这个我们后面讲。然后在store上定义了一个对象getters,然后遍历之前,registerGetters注册的getter,然后是这段代码
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// partial函数实现
export function partial (fn, arg) {
return function () {
return fn(arg)
}
} 首先是遍历所有getters,调用partial函数,返回了一个新函数,并把它放入computed对象中,后面的代码其实是做了这件事
$store.getter 等价于 $store._vm.getter
把getter代理到了一个新的Vue实例的computed对象上,这在后面的代码有所体现
const silent = Vue.config.silent
// 启动Vue的日志和警告
Vue.config.silent = true
store._vm = new Vue({
data: {
// 把state放在Vue的data中
$$state: state
},
computed // 把所有getter放在了computed中
}) 这段代码相信不会陌生,vuex之所以能够响应式,原因就在这里,我们通过调用mutation,修改了state,会触发页面更新,其实是Vue的帮助
strict
我们继续看后面的代码
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// 强制getters重新计算
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 防止重复创建Vue实例(个人理解)
Vue.nextTick(() => oldVm.$destroy())
} 首先是判断strict是否为true, 表示是严格模式,如果直接更改state,会报错,我们看一下它的实现
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
} 很关键的是中间的箭头函数,我们可以直接看一下Vue源码的实现,它是如何实现修改state报错
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 很关键的属性
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
} 这段代码有个地方很关键,options.user = true,它被传入了Watcher对象中,还有我们传入了箭头函数cb
我们看看Watcher哪里有使用到user属性
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// ...
}
我先说一下,这个run方法在什么时机调用的,它是在set属性访问器内部调用notify之后,watcher会调用自身的update方法,然后run就会被调用,可能说的不太清楚,如果各位有时间可以看一下,这里只针对strict原理来讲
下面我们只看这段代码
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
} 我们知道之前传入的user属性为true, 如果调用回调是一定会抛出错误的
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
} 这就是strict模式下,直接修改state会报错的原因
讲完这些,其实后面的代码就简单略过了,也不是很重要(懒?)
然后我们来看Store构造函数中最后一点内容
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
} 首先调用了所有的plugin,并传入了store对象,关于plugin的用法官方文档都有介绍。然后关于useDevtools内容我就不讲解了,它和devTool相关
终于讲完了初始化,我们开始讲Vuex的一些API
API
我们按照官方文档一个个来
1. commit
commit的用法就不用介绍了,直接看源码
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(() => {
// 遍历type对应的mutation数组
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 遍历所有订阅,并传入mutation对象和状态
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(type: string, payload?: any, options?: Object) commit(mutation: Object, options?: Object)
然后是下面这段
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
} 如果commit的mutation不存在的话,就会报出警告,并返回不做处理
this._withCommit(() => {
// 遍历type对应的mutation数组
entry.forEach(function commitIterator (handler) {
handler(payload)
})
}) _withCommit方法前面也有讲过,简单点说其实就是调用传入的回调函数,这里循环调用了mutation,至于为什么是数组,前面有讲到,是在registerMutation方法
我们继续来看
// 遍历所有订阅,并传入mutation对象和状态
this._subscribers.forEach(sub => sub(mutation, this.state))
// silent属性已经被删除,不让使用
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'
)
} this._subscribers属性也是在Store对象的构造函数初始化时创建的一个数组,看到这个数组的名字,不用多说肯定是发布订阅模式,然后循环调用订阅的回调函数,它是在mutation被调用后执行, 但是在哪里订阅的呢,其实是在subscribe方法,它也是Vuex的一个API,下面我们来具体讲讲
2. subscribe
订阅 store 的 mutation。handler 会在每个 mutation完成后调用,接收 mutation 和经过 mutation 后的状态作为参数
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
} function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
} 这就是一个简单的发布订阅模式的应用,把回调存储在了订阅数组中,其中genericSubscribe方法利用了闭包,返回了一个函数,调用它之后就可以取消订阅,其实还有其他的订阅方法,subscribeAction
3. subscribeAction
subscribeAction (fn) {
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
} 判断是否是一个函数,如果是默认为before函数,也就是在dispatch调用action之前调用,如果是{after: fn}就会在action之后调用
4. dispatch
// 执行了beforeActions所有回调
// 执行所有actions,并拿到所有promise返回的结果
// 执行了afterActions所有回调
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
}
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
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 result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
} 前面关于对象统一,以及是否存在action的判断就不讲了
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
} 然后过滤筛选获取到了订阅的一些before函数,也就是在调用action之前调用,并传入了action, action = { type, payload }以及state
5. watch
响应式地侦听 fn 的返回值,当值改变时调用回调函数。fn 接收 store 的 state 作为第一个参数,其 getter 作为第二个参数。最后接收一个可选的对象参数表示 Vue 的 vm.$watch 方法的参数。
watch (getter, cb, options) {
if (process.env.NODE_ENV !== 'production') {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
}
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
// Store构造函数初始化时
this._watcherVM = new Vue() 这里给侦听函数里的,getter传入了state和getters, 当state发生变化时,侦听函数的返回值也发生了变化,值改变后就会触发cb回调函数, 关于`vm.watch`](https://cn.vuejs.org/v2/api/#vm-watch)
6. replaceState
替换 store 的根状态,仅用状态合并或时光旅行调试。
this._withCommit(() => {
this._vm._data.$$state = state
}) 直接替换掉了$$state原本状态
7. registerModule
可以注册模块,例子:
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
}) registerModule (path, rawModule, options = {}) {
if (typeof path === '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)
} 首先时统一处理了一下path和一些断言,然后调用了register方法installModule方法,resetStoreVM方法,这几个方法前面都有讲到,相当于又创建了一个Store对象,流程也差不多
8. unregisterModule
卸载一个动态模块。
unregisterModule (path) {
if (typeof path === '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(() => {
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
resetStore(this)
} 前面是对path模块名进行了处理以及断言是否是数组,然后调用unregister
this._modules.unregister(path)
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return
parent.removeChild(key)
} 这里获取到了传入模块名,也就是path的父模块,然后获取子模块判断是否存在runtime属性,这个属性是干嘛的,我也不是很清楚,希望又大佬解惑(菜 !- -,没办法啊)
parent.removeChild(key)
removeChild (key) {
delete this._children[key]
} 最后删除了子模块,也就是我们要删除的模块
9. hotUpdate
热替换新的 action 和 mutation
官方的例子
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
Vue.use(Vuex)
const state = { ... }
const store = new Vuex.Store({
state,
mutations,
modules: {
a: moduleA
}
})
if (module.hot) {
// 使 action 和 mutation 成为可热重载模块
module.hot.accept(['./mutations', './modules/a'], () => {
// 获取更新后的模块
// 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 加载新模块
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})
} 热模块更新源码如下
hotUpdate (newOptions) {
this._modules.update(newOptions)
resetStore(this, true)
} this._modules.update(newOptions)方法是在module-collection.js文件中定义
update (rawRootModule) {
update([], this.root, rawRootModule)
}
function update (path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}
// update target module
targetModule.update(newModule)
// update nested modules
if (newModule.modules) {
for (const key in newModule.modules) {
// 如果传入的配置中没有该模块就报错
if (!targetModule.getChild(key)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
}
以上代码总的来说就是递归遍历模块,并更新模块,其中涉及到三个update方法,大家不要弄混。
update([], this.root, rawRootModule)
主要传入了,一个空数组,原本的根模块对象,要用来替换的模块配置
function update (path, targetModule, newModule) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, newModule)
}
// update target module
targetModule.update(newModule)
// update nested modules
if (newModule.modules) {
for (const key in newModule.modules) {
// 如果传入的配置中没有该模块就报错
if (!targetModule.getChild(key)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
} 递归遍历,原本的模块树,使用新模块替换掉原本模块
以上代码中还有一个模块中的update方法,即targetModule.update(newModule)
// update target module
targetModule.update(newModule)
// module.js
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
} 这个方法其实很简单,替换掉了原本的模块。
辅助函数
mapXXX方法都在helper.js文件中
// helper.js
export const mapState = normalizeNamespace((namespace, states) => {
//..
})
export const mapMutations = normalizeNamespace((namespace, mutations) => {
// ..
})
// ... 可以看到他们都调用了normalizeNamespace方法,我们知道mapXxx是一个方法,所以它一定会返回一个方法
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
} 这个方法实际上是对参数进行了处理,判断如果namespace不是字符串,也就是说它可能不存在,namespace就设置为一个空字符串,比如这样
{
computed: {
...mapState(['username'])
}
}如果传入了namespace字符串,并且最后没有斜杠,就自动帮它加上,最后才是调用真实的mapXXX,比如这样
{
computed: {
...mapState('user/', ['username'])
}
}接下来我们看一下mapState实现
mapState
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
}) 首先又是调用了一个normalizeMap方法,传入了我们需要获取的states,normalizeMap实现如下
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
} 这段代码看起来可能有点复杂,举个例子
normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap(['user', 'count']) => [ { key: 'user', val: 'user' }, { key: 'count', val: 'count' }]
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ] 然后我们回到之前的代码
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
}) 细心的童鞋可能注意到了,整个mapState返回的是一个对象,其形式如下,其他mapMutations,mapActions都可以这样
mapState('user', ['username', 'password'])
{
username: function(){},
password: function(){}
}
mapMutation('count', ['increment']) 现在知道为啥mapState要写在computed里了吧!原因就在这里。为了方便我就直接用注释分析了
res[key] = function mappedState () {
// store对象中的state,这个state是根state
let state = this.$store.state
// 根getters
let getters = this.$store.getters
// 如果传入了namespace
if (namespace) {
// 调用getModuleByNamespace方法,源码实现在下方,它返回namespace对应的模块
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 有看过前面源码应该记得,很多方法和对象都挂载到了context属性上
state = module.context.state
getters = module.context.getters
}
// 调用val或获取state
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
}) function getModuleByNamespace (store, helper, namespace) {
// _modulesNamespaceMap属性是不是很眼熟?
// 它是在Store类的installModule方法中使用到,记录了namespace对应的module
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
} 上面这些代码有几个注意点
getModuleByNamespace方法中的store._modulesNamespaceMap[namespace]是在installModules中进行的初始化mapState是可以传入回调函数的{ computed: mapState({ // 箭头函数可使代码更简练 count: state => state.count, // 传字符串参数 'count' 等同于 `state => state.count` countAlias: 'count', // 为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } }) }
mapMutations
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
}) 其他相同的代码就不讲了,关键看下面的
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
} 这段代码其实和mapState里的相差不大,都是获取到commit,如果有namespace就获取模块里的commit,最后调用commit,它也可以传入一个回调函数,不过,举个例子
methods: {
...mapMutations(['increment']),
//等价于
...mapMutations({
add: function(commit, ...args){
commit('increment', ...args)
}
}),
// 等价于
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
// 组件中调用
this.add(1) mapGetters
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
// 如果namespace存在但是没有找到对应的模块 就直接返回,不做处理
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
// 如果没有找到对应的getter会报错并返回
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
}) mapGetters和其它实现有所区别
所有模块的
getters都被代理在store对象中,所以直接使用getter的key和namespace拼接获取到对应的getter;具体在哪代理可以参见// store.js 的makeLocalContext方法里的实现 Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } })
getter不支持传入函数
mapActions
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
}) mapActions的实现和mutation的实现一模一样?确实是这样。。。下面只说下用法
methods: {
...mapActions(['increment']),
//等价于
...mapActions({
add: function(dispatch, ...args){
dispatch('increment', ...args)
}
}),
// 等价于
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
// 组件中调用
this.add(1) createNamespacedHelpers
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
}) 官方例子
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
} 对于这个createNamespacedHelpers如何实现,我想大家应该看的懂吧
总结
终于分析完了Vuex的源码,完成这篇文章也是没事抽出空闲时间写出来的,可能会有错别字,分析错误或者有些我不知道的,欢迎大家指正,阅读源码也使我学到了很多东西,让我从陌生,逐渐开始得心应手,一直到现在,我对于源码不再是单纯的为了面试,而是一种兴趣,谢谢大家观看
下一章
逐行级源码分析系列(二) Redux和React-Redux源码(正在写作)
未完待续。。。