3.2.4 注册模块的actions
// 注册模块的所有actions module.forEachAction((action, key) => { const type = action.root ? key : namespace + key // 判断是否需要在命名空间里注册一个全局的action const handler = action.handler || action // 获取actions对应的函数 registerAction(store, type, handler, local) })
遍历模块的所有 actions
方法,其中对于 type
和 handler
的处理主要是为了兼容两种写法:
// 第一种写法: actions: { func(context, payload) { // 省略业务代码... } } // 第二种写法: actions: { func: { root: true, handler(context, payload) { // 省略业务代码... } } }
当采用第二种写法,并且 root = true
时,就会将该 actions
方法注册到全局上,即前面不加上任何的命名空间前缀
再来看看 registerAction
方法里具体实现了什么
// 注册actions方法,接收两个参数:context(包含了上下文中的dispatch方法、commit方法、getters方法、state)、传入的参数payload function registerAction (store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []) // 通过store._actions 记录所有注册的actions entry.push(function wrappedActionHandler (payload) { let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload) // 若返回值不是一个promise对象,则包装一层promise,并将返回值作为then的参数 if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
与 mutations
类似,先从 store._actions
获取入口 entry
,然后将当前的 actions
进行包装处理后添加到 entry
的末尾。actions
方法接收两个参数,即 context
和我们传入的参数 payload
,其中 context
是一个对象,里面包含了 dispatch
、commit
、getters
、state
、rootGetters
、rootState
,前4个都是在当前模块的上下文中调用的,后2个是在全局上调用的
最后对于 actions
的返回值还做了一层处理,因为 actions
规定是处理异步任务的,所以我们肯定希望其值是一个 promise
对象,这样方便后续的操作。所以这里对 actions
方法的返回值做了一个判断,如果本身就是 promise
对象,那么就直接返回 ;若不是,则包装一层 promise
对象,并将返回值 res
作为参数返回给 .then
同样的,actions
方法也是可以重名的
3.2.5 注册模块的getters
// 注册模块的所有getters module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) })
与上面的类似,这里就不多说了,直接跳到 registerGetters
方法
// 注册getters function registerGetter (store, type, rawGetter, local) { if (store._wrappedGetters[type]) { // 若记录过getters了,则不再重复记录 if (__DEV__) { console.error(`[vuex] duplicate getter key: ${type}`) } return } // 在store._wrappedGetters中记录getters 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
并不像 mutations
和 actions
一样去获取一个 entry
,而是直接查看 store._wrappedGetters[type]
是否有对应的 getters
,若有,则不再重复记录 ; 否则将 getters
包装一下存在 sotre._wrappedGetters
中,其中经过包装后的 getters
接收4个参数,即 state
、getters
、rootState
、rootGetters
,前2个分别表示当前上下文中的 state
和 getters
,后2个分别表示根模块的 state
和 getters
所以我们在使用 Vuex
时,调用子模块的 getters
时是这样的:
const store = Vuex.Store({ state: { a: 1, b: 2 }, getters: { addA(state) { return state.a + 1 } }, modules: { // 子模块A ModuleA: { state: { c: 3 }, getters: { sum(state, getters, rootState, rootGetters) { console.log(state.c) // 3 console.log(getters.addC) // 4 console.log(rootState.b) // 2 console.log(rootGetters.addA) // 2 }, addC(state) { return state.c + 1 } } } } })
最后我们再次得出一个结论,getters
是不能重名的,并且前一个命名的不会被后一个命名的所覆盖
3.2.6 递归注册子模块
// 递归注册子模块 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })
然后就是判断当前的模块里有没有嵌套的子模块了,有的话就将子模块的名称添加到 path
末尾,然后把相应的参数传入 installModule
方法,重新走一遍本文中 3.2
里所有的流程
3.3 注册vm
上面已经将模块的注册完毕了,看一下 constructor
中下一行代码是什么:
resetStoreVM(this, state)
跳到相应的方法中去看一下:
// 初始化vm function resetStoreVM (store, state, hot) { const oldVm = store._vm store.getters = {} // 在实例store上设置getters对象 store._makeLocalGettersCache = Object.create(null) // 清空本地缓存 const wrappedGetters = store._wrappedGetters const computed = {} // 遍历getters,将每一个getter注册到store.getters,访问对应getter时会去vm上访问对应的computed forEachValue(wrappedGetters, (fn, key) => { computed[key] = partial(fn, store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) const silent = Vue.config.silent Vue.config.silent = true // 使用Vue实例来存储Vuex的state状态树,并利用computed去缓存getters返回的值 store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent // 启用严格模式的监听警告 if (store.strict) { enableStrictMode(store) } // 若存在旧的vm, 销毁旧的vm if (oldVm) { if (hot) { // 解除对旧的vm对state的引用 store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } }
这个方法里主要做的就是生成一个 Vue
的实例 _vm
,然后将 store._makeLocalGettersCache
里的 getters
以及 store.state
交给一个 _vm
托管,即将 store.state
赋值给 _vm.data.$$state
,将 store._makeLocalGettersCache
通过转化后赋值给 _vm.computed
,这样一来,state
就实现了响应式,getters
实现了类似 computed
的功能
因为生成了新的 _vm
,所以最后通过 oldVm.$destory()
将旧的 _vm
给销毁掉了
值得注意的是,其将 sotre.getters
的操作放在了这个方法里,是因为我们后续访问某个 getters
时,访问的其实是 _vm.computed
中的内容。因此,通过 Object.defineProperty
对 store.getters
进行了处理
3.4 访问 state 、mutations 、actions
到此为止,已经实现了可以通过 store.getter.某个getters
来使用 getters
,那么如何访问 state
、mutations
、actions
呢?
3.4.1 访问 state
通过搜索,在 Store
类中定义了一个 get
函数,用于处理 store.state
的操作:
get state () { return this._vm._data.$$state }
可以很清楚地看到,当我们访问 store.state
时,就是去访问 store._vm.data.$$state
,与刚才介绍 _vm
时说的一样
3.4.2 访问 mutations
其实 mutations
的访问在一开始就触及到了,只不过当时只是提了一嘴,因为当时直接来看可能不会太明白
const store = this const { dispatch, commit } = this this.dispatch = function boundDispatch (type, payload) { return dispatch.call(store, type, payload) } this.commit = function boundCommit (type, payload, options) { return commit.call(store, type, payload, options) }
在 Store
中,对 store.commit
和 store.dispatch
方法做了一层处理,将该方法的调用指向了 store
,先来看看 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] // 查找_mutations上是否有对应的方法 // 查找不到则不执行任何操作 if (!entry) { if (__DEV__) { console.error(`[vuex] unknown mutation type: ${type}`) } return } // 若有相应的方法,则执行 this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) this._subscribers .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe .forEach(sub => sub(mutation, this.state)) if ( __DEV__ && options && options.silent ) { console.warn( `[vuex] mutation type: ${type}. Silent option has been removed. ` + 'Use the filter functionality in the vue-devtools' ) } }
首先通过 unifyObjectStyle
方法对传入的参数进行了处理,来看一下这个方法是干什么的
function unifyObjectStyle (type, payload, options) { if (isObject(type) && type.type) { options = payload payload = type type = type.type } if (__DEV__) { assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`) } return { type, payload, options } }
使用过 Vuex
的应该都知道,commit
有两种提交方式:
// 第一种提交方式 this.$store.commit('func', 1) // 第二种提交方式 this.$store.commit({ type: 'func', num: 1 })
其先对第一个参数进行判断是否为对象,是的话就当作对象提交风格处理,否则的话就直接返回
在处理完参数以后,根据 type
从 store._mutations
上获取到 entry
,前面分析过了,mutations
方法是以数组形式存储的,所以可能有多个方法。然后在 _withCommit
方法中遍历 entry
依次执行 mutations
方法,这是因为 Vuex
规定 state
的改变都要通过 mutations
方法,store._committing
这个属性就是用来判断当前是否处于调用 mutations
方法的,当 state
值改变时,会先去判断 store._committing
是否为 true
,若不为 true
,则表示 state
的值改变没有经过 mutations
方法,于是会打印警告⚠️ 信息
而 this._subscribers
这段代码我暂时还不清楚是干什么的,通过词义,目测应该是一个存放订阅的东西吧,就先放着不管了,等后续回来再看
3.4.3 访问 actions
dispatch (_type, _payload) { // check object-style dispatch const { type, payload } = unifyObjectStyle(_type, _payload) const action = { type, payload } const entry = this._actions[type] // 查找_actions上是否有对应的方法 // 查找不到则不执行任何操作 if (!entry) { if (__DEV__) { console.error(`[vuex] unknown action type: ${type}`) } return } try { this._actionSubscribers .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe .filter(sub => sub.before) .forEach(sub => sub.before(action, this.state)) } catch (e) { if (__DEV__) { 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 new Promise((resolve, reject) => { result.then(res => { try { this._actionSubscribers .filter(sub => sub.after) .forEach(sub => sub.after(action, this.state)) } catch (e) { if (__DEV__) { console.warn(`[vuex] error in after action subscribers: `) console.error(e) } } resolve(res) }, error => { try { this._actionSubscribers .filter(sub => sub.error) .forEach(sub => sub.error(action, this.state, error)) } catch (e) { if (__DEV__) { console.warn(`[vuex] error in error action subscribers: `) console.error(e) } } reject(error) }) }) }
前半部分与 commit
方法类似,就不多说了
代码中又出现了 this._actionSubscribers
,与 commit
中的也类似,可能这里是存放 actions
的订阅者的东西,所以这些都先不看了
其中变量 result
,先判断 entry
的长度,若大于1,则表示有多个异步方法,所以用 Promise.all
进行包裹 ; 否则直接执行 entry[0]
最后创建并返回了一个新的 promise
,内部判断了 result
的状态,成功则执行 resolve
,失败则执行 reject
到此为止,我们已经实现了 store.state
、store.getters
、store.commit
、store.dispatch
的调用了
3.5 插件的调用
继续看 constructor
中的代码(这段代码也是整个 Store
类的构造函数中最后的一小段代码了)
// 依次调用传入的插件 plugins.forEach(plugin => plugin(this)) const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools // 使用vue的开发插件 if (useDevtools) { devtoolPlugin(this) }
首先就是遍历创建 Store
类时传入的参数 Plugins
,依次调用传入的插件函数(当然一般我们都没有传入,所以 Plugins
默认是空数组)
然后就是调用 devtoolPlugin
方法啦,根据导入的路径我们去到相应的文件
// 文件路径:./plugins/devtool.js const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {} const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__ export default function devtoolPlugin (store) { if (!devtoolHook) return store._devtoolHook = devtoolHook devtoolHook.emit('vuex:init', store) devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) }) store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }, { prepend: true }) store.subscribeAction((action, state) => { devtoolHook.emit('vuex:action', action, state) }, { prepend: true }) }
看了半天,搜索了半天,都没有找到哪个文件里有 __VUE_DEVTOOLS_GLOBAL_HOOK__
,应该是 dev-tools
插件里定义的,为了保证 Vuex
的源码阅读进度,就先舍弃阅读 dev-tools
插件的内容了
3.6 其它方法
整个 Store
实例生成的全过程差不多就是这样了,另外还会发现,其实有很多方法都没有被用到,但是却被定义出来了,这里可以稍微列举几个简单地看一下
3.6.1 更新 state
// 在store._committing = true 的状态下更新一下state replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
一目了然,这是提供了一种直接修改 state
的方法,并且不会打印警告信息
3.6.2 注册、卸载模块
// 注册模块 registerModule (path, rawModule, options = {}) { if (typeof path === 'string') path = [path] if (__DEV__) { assert(Array.isArray(path), `module path must be a string or an Array.`) assert(path.length > 0, 'cannot register the root module by using registerModule.') } this._modules.register(path, rawModule) installModule(this, this.state, path, this._modules.get(path), options.preserveState) // reset store to update getters... resetStoreVM(this, this.state) } // 卸载模块 unregisterModule (path) { if (typeof path === 'string') path = [path] if (__DEV__) { 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) }
3.6.3 重置 store 实例
// 重置store,即注册模块、生成vm等操作 function resetStore (store, hot) { store._actions = Object.create(null) store._mutations = Object.create(null) store._wrappedGetters = Object.create(null) store._modulesNamespaceMap = Object.create(null) const state = store.state // init all modules installModule(store, state, [], store._modules.root, true) // reset vm resetStoreVM(store, state, hot) }
将所有的状态都清空,然后重新执行一边 installModule
和 resetStoreVM
,这一般在模块结构变化以后调用,例如某个模块被卸载