前言
由于最近在研究SPA状态管理的内容,因此对Vuex
的源码进行了拜读,所幸Vuex
的源码并不是特别多,因此本文将对Vuex 3.1.3版本
的主要逻辑和实现进行分析和记录。
要知道,如果漫无目的去阅读源码,将会非常的枯燥,而带着疑问去寻找答案,发现答案的过程将会让人印象更加深刻。笔者在开始阅读源码前,也是带着几个问题开始的:
- Vuex中的state如何做到响应式?
- 命名空间是怎么实现的?
- Vuex的实例是怎么挂载到Vue中的?
- Vuex怎么实现从commit中更新state的约束?
- 插件机制是什么实现的?
有了这些问题,阅读源码就更有方向性了。
初始化
首先得Vuex
的初始化开始说起:
// store.js
import Vuex from 'vuex';
import Vue from 'vue';
Vue.use(Vuex);
export default new Vuex.Store({
strict: true,
state: {}
});
// 入口main.js
import Vue from 'vue';
import store from './store.js';
new Vue({
...,
store
});
这些代码是我们在项目中初始化Vuex
的时候,非常常见的一般写法;问题来了,为什么要这样写呢?这里涉及到Vue
插件一般的注册方法:
- 先从Vue.use开始看,它主要是调用插件的install方法,并将插件注册到
_installedPlugins
的内部变量中,防止重复安装; - 插件install方法将
vuexInit
方法挂载到Vue
初始化生命周期中,这里,Vue2.0及以上通过mixin挂载,而2.0以下则通过函数劫持的方式处理;
function vuexInit () {
const options = this.$options
if (options.store) {
// Vue初始化后,store实例将可以通过this.$store访问
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
- 通过
new Vuex.Store()
进行Store的实例初始化,并且将Store实例导出; - 导入Store实例,作为Vue的options的参数,此时在上述
vuexInit
方法里,从options.store中是可以获取到该Store实例的,接着该实例挂载到this.$store
中; - 至此,实例挂载完成!
Store构造函数执行流程
在上述挂载的过程中,还涉及到了Store
的实例化,实例化Store
是一个非常关键的流程,在其构造函数中,进行了模块的安装以及模块的收集等的逻辑,并形成了Store
的关键API,下面将逐步分析(此处就不贴源码了):
- 初始化私有变量,如_modules、_actions、_mutations、_wrappedGetters、_subcribers、_actionSubscribers、_modulesNamespaceMap等,私有变量的具体作用,将在[数据模型]()章节中讲解;
- 初始化公有API:
dispatch
、commit
; - 初始化模块收集对象
ModuleCollection
,并递归收集定义的子模块; - 初始化root module(根模块)、并且递归安装子模块,安装过程中将会进行namespaced模块登记、将模块state注入到父级的state中、本地化模块内的
getters
`mutations`actions
,详细将在[installModule]()章节中进行分析; - 对store中的state进行响应式处理,并且将getters处理成computed属性
- 执行plugins
数据模型
在Vuex
中,存在着三种比较重要的数据结构,这些数据结构构造了整个状态管理,在执行Store
构造函数的过程中,ModuleCollection
将模块存储在Module
中,并构造出一棵模块树
,下面就列举一下这三个主要的数据结构Store
、ModuleCollection
、Module
:
- Store(仅列举部分)
- ModuleCollections
- Module
- 各个模块在注册后,最终会形成类似如下的树状结构:
这里可能有人会有疑问了:我们获取状态的时候,数据结构没有这么多层包裹的呀?是的,确实没有这么多层包裹,这是因为在注册模块的同时,Vuex
还对Module中的state进行优化,将子模块的state挂载到父模块的state中(具体是在installModule中实现),随后将根模块的state挂载到Store的state中,使得你不需要通过这么多层去读取state。
模块与模块收集
通过上文,对于Vuex
构造出来的模块结构有了大概的了解,那么模块是如何被创建和收集的呢?从源码看是存在两种方式的:
- 通过
new Vuex.Store
构造函数创建 - 通过调用
store.registerModule
创建
这两者从根源上,底层都是调用了ModuleCollection
对象的register
方法,在register
方法中,会对Module
对象进行创建,并且将新的对象添加到父模块的children中,随后递归添加rawModule
中定义的module
子模块。
register (path, rawModule, runtime = true) {
// 创建Module对象
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 处理根节点,第一次创建时执行
this.root = newModule
} else {
// 将新模块挂载到父模块下
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 递归注册子模块
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
大家可能会对rawModule
比较迷惑,其实这个就是我们在业务代码中定义模块写的代码,例如下面registerModule中的参数代码就是我们的rawModule。
// 第二个参数,就是rawModule
store.registerModule('subModule', {
state: {}
actions: {}
getters: {}
mutations: {}
});
Store中的私有变量
Store
的构造函数中定义了一些私有变量,用于收集状态和方法,下面就介绍主要的几个私有变量
- _modules:ModuleCollection实例,存储Module树;
- _actions:存储所有的actions,_actions的key可被namepaced;
- _mutations:存储所有的mutations,_acitons的key可被namepaced;
- _wrappedGetters:存储所有的getters,_wrappedGetters的key可被namepaced;
- _subscribers:存储所有的subscribe的callback;
- _actionSubscribers:存储所有的subscribeAction的callback;
- _modulesNamespaceMap:存储所有的namespaced后module;
- _makeLocalGettersCache:缓存模块getters,用于在子模块内获取子模块自己的getter;
这里,可被namespaced是指其key值是带有命名空间前缀的。
命名空间
Vuex
使用命名空间对模块间进行隔离,不同命名空间的state
、actions
、mutations
、getters
调用,需要添加模块名,在命名空间内部则不需要声明;
这里需要留意的是,源码中对于actions
和mutations
的处理,是允许相同命名空间的相同actions
和mutations
,执行过程时这些actions
和mutations
都会被处理;
function registerMutation (store, type, handler, local) {
// 此处维护一个数组,相同类型的mutation将会放在同一个数据内
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
function registerAction (store, type, handler, local) {
// 此处维护一个数组,相同类型的actions将会放在同一个数据内
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload) {
...省略代码
})
}
另外,子模块namespace
的完整构造,是通过数组的reduce
累加器方法构造出来;
getNamespace (path) {
let module = this.root
// 从根节点,根据路径path拼接出路径
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
例如,getNamespace(['first', 'second'])
,则返回first/second/
;
关键函数
在Store
的函数中,有几个关键的函数需要特别介绍:
installModule
在上面Store
的构造函数中也有提到,installModule
是个非常重要的函数,这个函数中,主要做了一下事情:
- 通过
Vue.set
将子模块的state
挂载到父模块的state
中,并且使其具有响应式
,这就使得state的访问更加方便,不用经过Module
对象进行读取了;要知道Vuex
中模块是一棵树状结构,并且每个节点是一个Module
对象; - 为新模块构造独立的上下文,这里通过
makeLocalContext
实现(下文有提到); - 以带有模块前缀的key,注册
mutation
,并且允许声明相同的key名的mutation
; - 以带有模块前缀的key,注册
actions
,并且允许声明相同的key名的actions
; - 以带有模块前缀的key,注册
getter
; - 递归注册其子模块;
resetStoreVM
改方法将实现Store
中state
的响应式,并且将getters
作为computed
的属性:
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// 将getter作为computed的属性,使其具有lazy-caching机制
computed[key] = partial(fn, store)
// 这里定义store中的getters,getters对应computed的属性,也即对应wrappedGetters
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
Vue.config.silent = true
// 这里实现state的响应式
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
makeLocalContext
makeLocalContext
方法会为新模块构造独立的上下文,这里理解起来会比较奇怪,我们可以把独立上下文当做是Vuex
为新模块创造内部执行环境,这个执行环境不受外部影响;
当模块声明为namespaced后,我们在调用模块的state或actions时,都需要添加模块名:
// index为声明为namepsaced的子模块
this.$store.dispatch('index/somAction');
makdeLocalContext
使得在子模块内部使用state或者commit时,不需要声明模块名:
// 模块内部
actions: {
somAction({ commit }) {
// 这里就不需要声明模块名了
commit('someCommit');
}
}
事实上,这个local context
最终调用的依然是带有模块名前缀的API,只是创建local context
,内部帮助我们处理好这些模块名前缀的构造了;
在makeLocalContext
过程中,如果模块未声明为namespaced时,直接使用根模块的state、actions、commit、和state;
这里需要特别说明makeLocalContext
内部对于getters
的处理,声明为namespaced后,getters会通过makeLocalGetters
获取,makeLocalGetters
先看看缓存_makeLocalGettersCache是否存在,否则构造缓存。
_withCommt
function _withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
初次看这段代码,会不知道有什么用,而且会疑问_commiting
是有什么作用的,但是如果看完下面这段代码,大概有更加了解:
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 })
}
我们知道直接修改state技术上是可以的,但是会提示上述错误do not mutate vuex store state outside mutation handlers.
。
这里用到Vue中的$watch,监听了this._data.$$state`的修改,并且`deep`是未`ture`的,`this._data.$$state
指向的正式我们的rootState
,当我们修改了rootState
中的状态,就会有这样的抛错;
注意了,这段抛错是有条件的,就是store._commiting
为false
的时候;
所以在源码中涉及到对state的修改时,都会由_withCommit
进行包裹,这是因为_withCommit
中会将store._commiting
置为true
,从而不会抛错; commit
这个API内部的操作,正是有_withCommit
进行包裹的;
总结
最后,我们回顾一下开头提出的问题,我们试着来回答一下:
- Vuex中的state如何做到响应式?
答:是通过内部定义的Vue实例,实现响应式;
- 命名空间是怎么实现的?
答:命名空间是实现是通过namespaced后,使得state
、actions
、mutations
、getters
的key值具有命名空间的前缀,从而做到相互独立;访问时,需要通过声明命名空间前缀,才能匹配到对应的key值的实例或方法;关键逻辑在installModule时,构造的namespace前缀,并以此作为最终的key值,注册到Store
的私有变量中;
- Vuex的实例是怎么挂载到Vue中的?
答:是通过在vuexInit
方法,将options参数中的Store
实例,挂载到this.$store
中;而vuexInit
方法时通过Vue.use
时,调用插件的install方法,通过Vue.mixin
的方式,将改vuexInit
挂载到Vue的声明周期中;
- Vuex怎么实现从commit中更新state的约束?
答:是通过_commiting
和_withCommit
实现的;
- 插件机制是什么实现的?
答:插件机制是通过Store
的构造方法的最后,遍历调用plugins数组中的plugin方法,并且将Store
实例作为参数传入plugin中,使得plugin内部可与操作store;
好了,本次的源码分析,就到此结束,希望大家也有所其方法,其中的内容欢迎大家讨论和指正!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。