首先这篇文章也是本人第一次发这种技术文章,错别字,分析错误,不知道的东西在所不免,但愿你们指正,目前本人仍是一位即将大四的学生,写这个系列的目的也是为了记录在源码中学习,经过写博客让我更加熟悉了源码及其内部完完整整的实现,经过这篇文章也让我对vuex的源码变得很是熟悉,在写这完篇文章以前,由于时间缘由,断断续续写了两个星期,虽然已经看完了,可是要所有分析完并写出来,太耗费精力和时间。而后这个系列我打算按照这个顺序来写,我会坚持写下来。排版可能有点差,我会慢慢学习,若是里面有错误,大佬轻喷。。javascript
当咱们使用Vue.use
会调用vuex的install
方法,它的实现以下html
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
方法实现vue
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
}
}
}
复制代码
其实整个函数看起来彷佛有点复杂java
Vue.mixin({ beforeCreate: vuexInit })
复制代码
其实只是调用了这段代码,由于这是vue2.0版本及以上才有的方法,咱们这里只讨论vue2.0的状况,关于mixin
的用法,这里不作介绍,它为全部的组件添加beforeCreate
生命周期钩子react
下面咱们看一下vuexInit
方法的实现ios
// 这个方法的做用就是可让每一个组件都能经过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.$options
,这段代码,若是你们有看过vue源码的应该知道,这是mergeOptions
后的options
, 先是判断是否存在store
属性,若是不存在,就在父组件中查找,若是有就使用父组件中的$store
,经过这种方式,可以在组件之间造成一种链式查找,其实本质上是引用了,根组件中的store
,举个例子web
new Vue({
router,
store, // $store实际最终指向的都是这里的store
render: h => h(App)
}).$mount('#app')
复制代码
安装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.`)
}
复制代码
这段代码咱们不作讨论,相信你们也知道什么意思vuex
const {
//一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 做为惟一参数,能够监听 mutation(用于外部地数据持久化、记录或调试)或者提交 mutation (用于内部数据,例如 websocket 或 某些观察者)
plugins = [],
strict = false //使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数之外修改 Vuex state 都会抛出错误。
} = options
复制代码
上面这段代码,获取了咱们传入的配置plugins
和strict
,上面代码中标注有每一个属性的做用,关于详细的使用能够到官网查看,之后会有讲解express
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
作了哪些初始化
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
作了什么
其余并非很重要,咱们先不提,再回到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
方法
/** * * @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
方法
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
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
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对象上,
判断当前模块是否有命名空间,若是不是,就不作任何处理,不然调用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']()
复制代码
看完这些相信你们都明白了
调用了getNestedState
方法,这个方法想必不用多说,前面也有讲过,用来获取模块的父模块state
,并返回
咱们再回到一开始,调用makeLocalContext
的位置, 返回的local对象,最终放在了模块的context
属性上
const local = module.context = makeLocalContext(store, namespace, path)
复制代码
接下来咱们继续分析,后面的内容
// 遍历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
下面咱们继续看后面的代码
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
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
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的帮助
咱们继续看后面的代码
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
咱们按照官方文档一个个来
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,下面咱们来具体讲讲
订阅 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
subscribeAction (fn) {
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
}
复制代码
判断是不是一个函数,若是是默认为before
函数,也就是在dispatch
调用action
以前调用,若是是{after: fn}
就会在action
以后调用
// 执行了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
响应式地侦听 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
的用法,能够参考Vue的官方文档vm.$watch
替换 store 的根状态,仅用状态合并或时光旅行调试。
this._withCommit(() => {
this._vm._data.$$state = state
})
复制代码
直接替换掉了$$state
本来状态
能够注册模块,例子:
// 注册模块 `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
对象,流程也差很少
卸载一个动态模块。
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]
}
复制代码
最后删除了子模块,也就是咱们要删除的模块
热替换新的 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
实现
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
}
})
}
复制代码
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)
复制代码
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
不支持传入函数
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)
复制代码
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源码(正在写做)
未完待续。。。