Vuex 是 专为 Vue.js 开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可以预测的方式发生变化。
在开发过程中,我们经常遇到 多个视图依赖 同一个数据的情况,且不说 如果是 子孙组件 之间的 多层跨越式 传参非常繁琐,对于 兄弟组件 之间的数据传递更是无能为力。因此,Vuex 就诞生了,它是一个 全局单例模式 的状态管理,我们的组件无论在什么地方,都能获取状态或者出发行为。
Vuex 核心思想
Vuex 应用的核心就是 store(仓库)。store 就是⼀个容器,它包含着你的应用中大部分的状态(state)。
那么我们也可以定义一个全局的对象来挂在数据,比如 window,为什么需要使用 Vuex ?
那么 Vuex 和单纯的全局对象有以下 两点 不同:
1 Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变化,那么 相应的组件也会相应地得到高效更新。
2 你不能直接改变 store 中的状态。改变 store 中的状态的 唯⼀途径 就是 显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每⼀个状态的变化,从而让我们能够实现⼀些工具帮助我们更好地了解我们的应用。
Vuex 初始化
当我们引用 Vuex 的时候,其实是引用了一个 对象。
和 Vue-Router ⼀样,Vuex 也同样存在⼀个静态的 install 方法。
install 的逻辑很简单,把传入的 _Vue 赋值给 Vue 并执行了 applyMixin(Vue) 方法。
其实就 全局混⼊了⼀个 beforeCreated 钩子函数,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例,这也是为什么我们在组件中可以通过 this.$store 访问到这个实例。
实例化
我们在 import Vuex 之后,会实例化其中的 Store 对象,返回 store 实例并传入 new Vue 的options 中,也就是我们刚才提到的 options.store。
Store 对象的构造函数接收⼀个对象参数,它包含actions 、 getters 、 state 、 mutations 、 modules 等 Vuex 的核心概念, Store 的实例化过程分为 三个部分:初始化模块,安装模块 和 初始化 store._vm。
初始化模块
在 Vue 开发过程中,我们秉承组件化开发的理念,将页面分成了若干个组件,自然的,各个组件维护者自己的 state。Vuex 是一颗 单一状态树,所有的 state 都会集中在 store 中,那么为了更好的区分各个组件所对应在 store 中的位置,我们也可以同样组件化的区分 store,这样,我们的组件 和 store 就能更好的对应,方便我们自己查找和开发。
Vuex 允许我们将 store 划分为 模块(module),每个模块就是一个小的 store,每个模块拥有自己的state 、 mutation 、 action 、 getter ,甚⾄是嵌套子模块——从上至下进行同样方式的分割, 这就像我们常说的 麻雀虽小五脏俱全。
模块 是一个 树形 的数据结构, store 可以理解为一个 root module,下面的就是 子模块,Vuex 要完成构建这棵树:
this._modules = new ModuleCollection(options);
实例化 ModuleCollection 的过程就是 执行了自己的 register 方法,register 接收 3 个参数,path 表示路径,我们整体目标是要构建⼀颗模块树, path 是在构建树的过程中维护的路径; rawModule 表示定义模块的原始配置; runtime 表示是否是⼀个运行时创建的模块。
register 内部首先 var newModule = new Module(rawModule, runtime);
Module 是 用来描述 单个模块的类。其代码
对于每个模块 this._rawModule = rawModule, 表示 每个模块的配置,this.state 表示每个模块定义的 state, this._children 表示 它的所有子模块。
register 实例化 Module 后,判断当前路径 path 的长度,首次调用的时候 this.register([], rawRootModule, false), 传入的是 空数组,则说明它是 根模块,this.root = newModule,赋值给 root;
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}})
我们的 path 就 是 a 和 b
否则就要生成 父子关系
var parent = this.get(path.slice(0, -1));
parent.addChild(path[path.length - 1], newModule);
最后也是最关键的,判断是否有 子模块,递归 调用 register 生成树
if (rawModule.modules) {
forEachValue(rawModule.modules, function (rawChildModule, key) {
this$1.register(path.concat(key), rawChildModule, runtime);
});
}
传入的 path 是 父模块的 path(我们将 modules 这个 object 的 key 作为 path), 通过 this.get(path.slice(0, -1) 方法,找到对应的模块,然后 addChild 建立父子关系。
root module 的下⼀层 modules ,它们的 parent 就是 root module ,他们会被添加的 root module 的 _children 中。每个⼦模块通过路径找到它的⽗模块,然后通过⽗模块的 addChild ⽅法建立父子关系,递归执行这样的过程,最终就建⽴⼀颗完整的模块树。
安装模块
初始化模块之后我们得到了一个树状的 store 仓库,接下来就是 执行安装模块的相关逻辑,它的目标就是对模块中的 state 、 getters 、 mutations 、 actions 做初始化工作
给我们 在
this._actions = Object.create(null);
this._actionSubscribers = [];
this._mutations = Object.create(null);
this._wrappedGetters = Object.create(null);
定义的这些数据初始化,其实就是 遍历这个树状结构,将每个小模块中的 actions、mutations、getters 集中在一起,建立我们的数据仓库,这样方便我们在使用。
代码入口
this._modules = new ModuleCollection(options);
var state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
然后就是核心的 installModule 方法, installModule 有 5 个参数, store 表示 root store ; state 表示 rootstate ; path 表示模块的访问路径; module 表示当前的模块, hot 表示是否是热更新。
首先是命名空间的概念,默认情况下,模块内部的 action 、 mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同⼀ mutation 或 action 作出响应。
如果我们希望模块具有更⾼的封装度和复⽤性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter 、 action 及 mutation 都会⾃动根据模块注册的路径调整命名。就像上面结果图片显示一样, 会带上 modules 的 key: 比如调用方法 commit['a/changeloading']。
1 首先根据 path 获取 namespace
比如 module a 和 module b 中 都有一个方法名为 changeItem 的方法,方法名字相同,比如 在 module a 或者 b 的内部,actions 中调用 mutation 都是 commit(‘changeItem’), 我们并没有写成commit(‘a/changeItem’),那么外部是如何区分它的呢,就是 makeLocalContext 它帮我们拼接了。
对于 getters 而言,如果没有 namespace ,则直接返回 root store 的 getters ,否则返回makeLocalGetters(store, namespace) 的返回值
2 接着 var local = module.context = makeLocalContext(store, namespace, path), 它的作用是 构建一个本地上下文环境;
makeLocalContext ⽀持 3 个参数相关, store 表示 root store ; namespace 表示模块的命名空间, path 表示模块的 path 。该⽅法定义了 local 对象,对于 dispatch 和 commit 方法,如果没有 namespace ,它们就直接指向了 root store 的 dispatch 和 commit ⽅法,否则会创建⽅法,把 type(就是我们 dispatch 和 commit 的方法名 ) ⾃动拼接上namespace(比如 commit(‘a/changeItem’), 就是 namespace/type ) ,然后执⾏ store 上对应的方法。
它重构了 dispatch 和 commit、getters, 如果 namespace 为 true 的话,就包装一下 commit,将 commit 的参数 加上我们的 namespace 的,这样就明确指定了我们所要调用的方法所在的模块。
3 遍历 mutation、action、getter
我们包装好 上下文环境之后就要遍历 该模块中的 mutation、action、getter,比如
⾸先遍历模块中的 mutations 的定义,拿到每⼀个 mutation 和 key ,并把 key 拼接上namespace ,然后执⾏ registerMutation ⽅法。该⽅法实际上就是给 root store 上的_mutations[types] 添加 wrappedMutationHandler ⽅法。注意,同一type 的 _mutations 可以对应多个方法。
最后还是 递归调用 installModule, 为 它的 子模块 进行注册。
module.forEachChild(function (child, key) {
installModule(store, rootState, path.concat(key), child, hot);
});
之前我们忽略了⾮ root module 下的 state 初始化逻辑,之前我们提到过 getNestedState ⽅法,它是从 root state 开始,⼀层层根据模块名能访问到对应 path 的 state ,那么它每⼀层关系的建⽴实际上就是通过这段 state 的初始化逻辑。
初始化 store._vm
Store 实例化的最后一步就是执行初始化 store._vm, 入口代码:
resetStoreVM(this, state);
resetStoreVM 的作⽤实际上是想 建立 getters 和 state 的联系,因为从设计上 getters 的获取就依赖了 state ,并且希望它的依赖能被缓存起来,且只有当它的依赖值发⽣了改变才会被重新计算。因此这⾥利⽤了 Vue 中用 computed 计算属性来实现。
resetStoreVM 首先遍历了 _wrappedGetters 获得每个 getter 的函数 fn 和 key ,然后定义了 computed[key] = () => fn(store) 。
fn(store) 相当于执⾏如下⽅法:
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
} ;
rawGetter 就是我们在执行 registerGetter(store, namespacedType, getter, local) 中传入的是用户定义的 getter 函数,它的前 2 个参数是 local state 和 local getters ,后 2 个参数是 root state 和 root getters 。
那么 computed[key] 就获取了 我们所有的 getters。
接着就是 实例化一个 Vue 实例 store._vm ,并把 computed 传⼊:
store._vm = new Vue({
data: {
$$state: state
},
computed
})
我们发现 data 选项⾥定义了 $$state 属性,⽽我们访问 store.state 的时候,实际上会访问Store 类上定义的 state 的 get ⽅法:
Object.defineProperties( Store.prototype, prototypeAccessors$1 );
var prototypeAccessors$1 = { state: { configurable: true } };
prototypeAccessors$1.state.get = function () {
return this._vm._data.$$state
};
它实际上就访问了 store._vm_data.$$state,那么 getters 和 state 如何建⽴依赖逻辑:
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
当我根据 key 访问 store.getters 的某⼀个 getter 的时候,实际上就是访问了store._vm[key] ,也就是 computed[key] ,在执行 computed[key] 对应的函数的时候,会执行rawGetter(local.state,...) 方法,那么就会访问到 store.state ,进而访问到store._vm_data.$$state ,这样就建立了⼀个依赖关系。当 store.state 发⽣变化的时候,下⼀次再访问 store.getters 的时候会重新计算。
再来看⼀下 strict mode 的逻辑
当严格模式下, store._vm 会添加⼀个 wathcer 来观测 this._data.$$state 的变化,也就是当store.state 被修改的时候, store._committing 必须为 true,否则在开发阶段会报警告。 store._committing 默认值是 false ,那么它什么时候会 true 呢, Store 定义了_withCommit 实例⽅法:
它就是对 fn 包装了⼀个环境,确保在 fn 中执⾏任何逻辑的时候 this._committing = true 。
所以外部任何⾮通过 Vuex 提供的接口直接操作修改 state 的⾏为都会在开发阶段触发警告。
总结
学到 vuex 的知识点,比如 初始化模块中 如何构建一个树结构, for 循环 遍历子元素(广度), 递归 生成深度。
为了代码的维护性和可读性,将 modules 分成小 module,然后通过初始化、安装模块 和 建立 getters 和 state 的联系。