逐行级源码分析系列(一) 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源码(正在写作)
未完待续。。。