Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
如果您还不熟悉 Vuex 的使用,请先去过一遍 Vuex 的 api ,跟着官方的 demo 走一遍,了解了 Vuex 的神奇之后,带着问题来一起看源码。
在进入源码之前,先列出几点对于Vuex
的问题:
action,mutation,getter 的回调函数里面的是怎么放入一个Store实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。
registerModule 和 unregisterModule 。
state 和 getter 是不是通过 new Vue() 来使其变为响应式的。
我们不能修改 state 是怎么做到的。
下面我们带着这几个问题来进入源码。
进入源码
分析源码使用的是 Vuex 2.3.1版本,建议 clone 下代码,使用本文提供的例子,跟着一起走,能加快理解。
目录结构
module-collection.js:按照传入的选项,进行 module 收集,建立 module 之间的关系;
module.js:生成一个 module 对象 包含私有属性和方法;
devtool.js:chrome 插件相关;
logger.js:订阅相关操作日志;
helpers.js:工具函数,mapState,mapMutations,mapGetters,mapActions;
index.js:入口文件;
mixin.js:在Vue实例上注册 options.store;
store.js:提供 store 的各个 module 构建安装;
util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法;
入口开始
1 | //index.js 入口文件 |
入口文件很简单,是vuex对外暴露的API:
Store:从我们 new Vuex.Store() 这样的方式,我们就该知道的是 Store 就是为我们生成状态树,并提供操作方法,定义操作规则的类;
install:Vue 插件安装,通过 Vue.use(Vuex) 执行,顺便看下 Vue.use 的代码;
mapState,mapMutations,mapGetters,mapActions 都是方便我们操作的函数。
1 | export function initUse (Vue: GlobalAPI) { |
plugin.install.apply(plugin, args) 调用了插件的 install 方法
1 | //store.js |
不难看出,当 Vue 执行了 install 函数,把 _Vue 作为参数传了进来,全局的 Vue 才有值,在用 Vue.use 便直接报错。
applyMixin:
1 | //mixins.js |
因为对每个实例和组件执行以下方法,这就带来了一些问题,
我们公共型的组件是不需要 store 的,
但 Vue 其实并无法区分公共型组件和业务型组件,便取了一个折中的方式,都给 store 。
先不用着急理解 vuexInit 方法,我们先回顾下开始是如何初始注入 Store 的。
1 | new Vue({ |
我们在 Vue 应用的根组件上注入了我们的 Store ,在生命周期执行到 init 或者 beforeCreated 的时候,执行了 vuexInit 步骤①,直接在当前根 Vue 上放了一个对象 store 并代理为 $store 。
我们创建子组件,编译过后其实执行的是 Vue.component() 方法,发现自身开始是没有 store 这个对象的,便“继承”一下父级的 store,通过 vueInit 步骤② $options.parent 来建立父子组件的关系。
这也就是所说的单一状态树,在单页面应用中,所有组件默认访问同一个 Store 实例,这个 Store 实例是会自动向下游组件渗透的
走进class Store
装载实例,配置 store :
1 | import Vue from 'vue' |
在这里先区分一下几个概念:
- options 下的第一层,是根模块( rootModule )。
- rootModule 下有一个孩子( _children )名字叫 module1 。
- module1 下有一个孩子( _children )名字叫 module2 。
你应该已经看出,我们的实例配置的地方也有几个值得注意的点:
- 三个 module 的 state 都有属性 name 。
- 三个 module 的 actions 里面都有个 fetchApi 的方法。
- 三个 module的 mutations 里面都有个 changeName 的方法。
- module1 和 module2 都有 nameGetter 。
- module2 有 namespaced 为 true 的配置。
很明显,我们想通过这个实例看看 namespaced 的重要性。
因为有new
操作符 Vuex.Store 就是个类,所以我们暴露出去的就是一个 Store 的实例,我们把整个状态树是一次性全部传入 Store 类的,传入的 OPTIONS 也就是下面的 options 。
1 | export class Store { |
整个constructor
分为五个大块,我们逐一分解。
① 添加 store 内部状态
- this._committing 标志一个提交状态,作用是保证对 Vuex 中 state 的修改只能通过 _withCommit 方法,在 mutation 的回调函数中,而不能在外部随意修改 state。
- this._actions 用来存储用户定义的所有 actions。
- this._mutations 用来存储用户定义所有 mutatins。
- this._wrappedGetters 用来存储用户定义的所有 getters。
- this._modules 用来存储所有运行时 module,并定义好他们的关系。
- this._modulesNamespaceMap 用来存储 options.namespaced 为 true 的 module。
- this._subscribers 用来存储所有对 mutation 变化的订阅者。
- this._watcherVM 是一个 Vue 对象的实例,调用 $watch 方法。
我们注意到 this._modules 是通过 ModuleCollection 类来生成的。
1 | //module-collection.js |
ModuleCollection
实例包含:
- 属性:root
- 原型方法:get,getNamespace,update (传入一个新 module,递归更新所有 module 的 actions,mutations,getters),register,unregister
进入Module类
来看下constructor
1 | export default class Module { |
Module
的实例包含:
- 属性:runtime,children,rawModule,rawState,namespaced;
- 原型方法:addChild,removeChild,getChild,update (更新 module 的 _rawModule),forEachChild,forEachGetter,forEachAction,forEachMutation;
最终在 Store 实例的this._modules
结构如下:
总结一下这两个类的作用:
就是构造一棵由 module 实例为节点的树( root 对象),并给这颗树和树的节点分别添加相应的属性和方法。
② dispatch和commit
1 | dispatch (_type, _payload) { |
拿到 _actions 入口的引用entry
。
entry
是个数组,长度等于1时直接执行里面的函数,当拿到的entry
长度大于1,就会遍历按顺序执行。
不妨做个假设,因为 rootModule 和 module1 的 action 名字相同,但他们都没有 namespaced 为 true 的设置,所以在这里可能是在同一个entry
里,mutation 同理。
1 | commit (_type, _payload, _options) { |
对于 actions 和 mutations 集合,是在 installModule函数 中添加的,稍后会看到。
现在我们来看下commit中的 _withCommit。
1 | _withCommit (fn) { |
只有 _withCommit 方法,才能更改当前 committing 的状态,也就是标识当前 state 是 Vuex 内部修改的。
#####③ 严格模式
在 resetStoreVM 中调用 enableStrictMode 函数会用到 this.strict 做判断来开启严格模式,具体在 resetStoreVM 中会说到。
有一点需要注意的是。严格模式下会观测所有 state 的变化,有一定性能开销,线上建议关闭。
④ installModule
把我们通过 options 传入的各种属性模块注册和安装,如果当前 module 中存在 modules 这个属性会被递归调用。
已实例来说,会被调用三次,分别是 rootModule,module1 和 module2。
1 | // ①构造函数调用 installModule(this, state, [], this._modules.root) |
这里比较重要的是 namespace,state,makeLocalContext(制作上下文),registerMutation,registerAction,registerGetter。
(1) namespace:
namespace 在只在一种情况下有值,
当前 module 的 namespaced 传入 true,并且当前 module 不是 rootModule
未设置 namespaced 为 true 和 rootModule (因为 path 为[]),得到的 namespace 都为空。
(2) state:
state 一直是 options 中传入的 state,递归之后变成如下,自动由 moduleName 来做了命名空间。
按照逻辑热更新状态下,安装 module,是不会改变 state,先记下逻辑,后面会深入。
(3) makeLocalContext (制作上下文):
1 | // 针对不同 module 制作局部的 dispatch, cimmit, getters 和 state方法 |
定义了一个 local 本地化对象,在上面挂载 getters 和 state 属性,dispatch,commit 方法,本质就是根据 noNamespace 做判断,来找到相应的方法和属性。
makeLocalGetters 和 getNestedState 方法都很简单,不细说了。
这里有两点其实还是很模糊:
- local 到底为我们做了什么?
- getters 和 state 里面的 store.getters 和 store.state 是什么时候绑定的。(目前为止还没有,绑定 getters 在 resetStoreVM 里,后面我们会看到。)
(4) registerMutation,registerAction,registerGetter:
1 | ... |
installModule 全部完成之后,我们的 Store 实例发生了变化
不难发现,由于 namespaced 为 false,我们无论是在 rootModule 还是在 module1 里面 dispatch fetchApi,都是会执行两次,fetchApi 里面的 commit 方法,也会触发两次,修改2个 module 的 state。
所以 module.context ( local 对象)就是为了 namespaced 而存在的。
本文开头的问题1,便找到了答案。
action mutation getter 的回调函数里面的是怎么放入一个 Store 实例相关对象的(包含:dispatch,commit,state,rootState,getters,rooteGetters等)。
在installModule阶段,首先制作了当前 module 的执行上下文,而上下文对象中包含了当前上下文的需要的属性(state,getters,rootState,rootGetters)和 dispatch 和 commit 方法。(其中要注意的是当 module 的 namespaced 为 true 时,dispatch 和 commit 触发的时候自动帮我们加上了 namespace ,这也就是我们能在 module2 中 commit(‘changeName’) 更改的是 module2 中 state 的 name 的)
在制作完上下文之后,便开始注册我们的 options 中的 action,mutation 和 getters,分别放入 Store 实例的 _actions,_mutations 和 _wrappedGetters 中,然后通过 local 对象和 Store 实例对象来组成传入工具对象,传入到我们定义的 action,mutation 和 getters 的回调函数中。
⑤ resetStoreVM
带着上面 getters 和 store.state 何时绑定的疑问,我们进入 resetStoreVM。
resetStoreVM方法就是初始化 store._vm,观测 state 和 getters 的变化。
1 | function resetStoreVM (store, state, hot) { |
问题3:
state 和 getter 是不是通过 new Vue 来使其变为响应式的。
- 是的,每次在执行 resetStoreVM 的时候都会 new Vue() 来创建一个新的 store ._vm, 然后把老的 vm 销毁,并且我们看到,state 和 getters 都是挂载在新的 store._vm 上的。
get state & set state
1 | class Store{ |
问题4:
我们不能直接修改 state 是怎么做到的。
- 当前我们设置 this.$store.state 的时候,会断言报错,并告诉我们只能通过 store.replaceState() 来修改 state,而 this.\$store.state 其实并未访问到真实 state ,只是代理 store._vm 上的 state,加上前面我们说的标识 committing,我就可以知道怎么修改 state 成功而不报错:
1 | changeState() { |
而我们看下上面提到的 store.replaceState()
方法:
1 | replaceState (state) { |
和我们预想的一样,所以外部修改 state,就需要调用 store.replaceState() 来假装自己是内部的修改,直接修改的就是一个断言函数,什么都没有。
enableStrictMode
resetStoreVM 中还通过 strict 判断来执行 enableStrictMode 函数,我们来看一下:
1 | function enableStrictMode (store) { |
store._vm.$watch开启了深监听,必然消耗性能的,所以线上建议关闭。
主要流程到这里就过完了,其他中还有些工具类方法和简单的方法,需要了解的再往下看,或者去源码中拜读就行。
读源码必然枯燥,但也是能学习尤大大代码的最短捷径,没有之一,无论是代码组织结构,功能模块解耦,函数职责单一,命名简单而又不简单,设计思路等都是我们非常直接借鉴的地方。
本文还有很多不足,有不对的地方望大家及时指出改正。
其他
动态注册 module 和注销 module。
Store类提供了两个方法:registerModule 和 unregisterModule。
也就是我们的问题2。
1 | registerModule (path, rawModule) { |
path 是数组,每次只取数组最后一位来做命名空间,register 方法其实是在指定父 module 上插入 _children :
- 如果 path 是 [‘module3’],则会创建一个和 rootModule 平级的 module。
- 如果 path 是 [‘module2’, ‘module3’],则会在 module2 的 _children 添加 module3。
this.state 是在 Store 实例化完成之后才可以访问的属性,所以也不难推断出,这两个方法是为 Store 实例服务的。
我们注意到在 unregisterModule
中,我们首先就从父级上删掉了我们要注销的 state,然后往下走到resetStore方法。
1 | function resetStore (store, hot) { |
可以知道
- 动态注销 module ,是要又从 rootModule 开始递归安装 module,很浪费性能。
- installModule 中 hot 传了 true,就不会改变 state,说明下面这段逻辑不会执行。
1 | function installModule (store, rootState, path, module, hot) { |
由于 hot 为 true,state 保持不变(之前已经被干掉了,也不需要变)。
subscribe
Store 实例提供了 subscribe 接口,作用是订阅 store 的 mutation。
1 | subscribe (fn) { |
接受的参数是一个回调函数,会把这个回调函数保存到 this._subscribers 上,并返回一个函数,当我们调用这个返回的函数,便可以解除当前函数对 store 的 mutation 变化的监听。
mapState
mapState 工具函数会将 Store 实例中的 state 映射到局部计算属性中。
使用示例:
1 | import { mapState } from 'vuex' |
mapState 函数可以接受一个对象,也可以接收一个数组,走进函数看一下:
1 | export function mapState (states) { |
函数首先使用了normalizeMap
来格式化传入的参数,进入这个函数看一下:
1 | function normalizeMap (map) { |
经过 mapState 函数调用后的结果,如下所示:
1 | import { mapState } from 'vuex' |
mapActions,mapGetters,mapMutations实现方式都差不多,这里就不多赘述,如果需要了解就去源码中查看。