Vuex源码阅读笔记

Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。有些同学可能会问,那我定义一个全局对象,再去上层封装了一些数据存取的接口不也可以么?

Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护。

Vuex 初始化

当我们在代码中通过 import Vuex from ‘vuex’ 的时候,实际上引用的是一个对象。

1
2
3
4
5
6
7
8
9
10
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}

和 Vue-Router 一样,Vuex 也同样存在一个静态的 install 方法,install 的逻辑很简单,把传入的 _Vue 赋值给 Vue 并执行了 applyMixin(Vue) 方法。

applyMixin 就是这个 export default function,它还兼容了 Vue 1.0 的版本,这里我们只关注 Vue 2.0 以上版本的逻辑,它其实就全局混入了一个 beforeCreate 钩子函数,它的实现非常简单,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例,稍后我们会介绍,这也是为什么我们在组件中可以通过 this.$store 访问到这个实例。

Store 实例化

我们在 import Vuex 之后,会实例化其中的 Store 对象,返回 store 实例并传入 new Vue 的 options 中,也就是我们刚才提到的 options.store 。

我们把 Store 的实例化过程拆成 3 个部分,分别是初始化模块,安装模块和初始化 store._vm 。

初始化模块

模块对于 Vuex 的意义:

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块——从上至下进行同样方式的分割。

所以从数据结构上来看,模块的设计就是一个树型结构,store 本身可以理解为一个 root module,它下面的 modules 就是子模块。

对于 root module 的下一层 modules 来说,它们的 parent 就是 root module,那么他们就会被添加的 root module 的 _children 中。每个子模块通过路径找到它的父模块,然后通过父模块的 addChild 方法建立父子关系,递归执行这样的过程,最终就建立一颗完整的模块树。

安装模块

初始化模块后,执行安装模块的相关逻辑,它的目标就是对模块中的 state、getters、mutations、actions 做初始化工作,它的入口代码是:

1
2
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)

installModule 实际上就是完成了模块下的 state、getters、actions、mutations 的初始化工作,并且通过递归遍历的方式,就完成了所有子模块的安装工作。

初始化 store._vm

Store 实例化的最后一步,就是执行初始化 store._vm 的逻辑,它的入口代码是:

1
resetStoreVM(this, state)

resetStoreVM 的作用实际上是想建立 getters 和 state 的联系,因为从设计上 getters 的获取就依赖了 state ,并且希望它的依赖能被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。因此这里利用了 Vue 中用 computed 计算属性来实现。

总结

我们要把 store 想象成一个数据仓库,为了更方便的管理仓库,我们把一个大的 store 拆成一些 modules,整个 modules 是一个树型结构。

每个 module 又分别定义了 state,getters,mutations、actions,我们也通过递归遍历模块的方式都完成了它们的初始化。

为了 module 具有更高的封装度和复用性,还定义了 namespace 的概念。最后我们还定义了一个内部的 Vue 实例,用来建立 state 到 getters 的联系,并且可以在严格模式下监测 state 的变化是不是来自外部,确保改变 state 的唯一途径就是显式地提交 mutation。

这一节我们已经建立好 store,接下来就是对外提供了一些 API 方便我们对这个 store 做数据存取的操作,下一节我们就来从源码角度来分析 Vuex 提供的一系列 API。

API

Vuex 提供的一些常用 API 包括数据的存取、语法糖、模块的动态更新等。

要理解 Vuex 提供这些 API 都是方便我们在对 store 做各种操作来完成各种能力,尤其是 mapXXX 的设计,让我们在使用 API 的时候更加方便,这也是我们今后在设计一些 JavaScript 库的时候,从 API 设计角度中应该学习的方向。

插件

Vuex 除了提供的存取能力,还提供了一种插件能力,让我们可以监控 store 的变化过程来做一些事情。

Vuex 的 store 接受 plugins 选项,我们在实例化 Store 的时候可以传入插件,它是一个数组,然后在执行 Store 构造函数的时候,会执行这些插件:

1
2
3
4
5
6
const {
plugins = [],
strict = false
} = options
// apply plugins
plugins.forEach(plugin => plugin(this))

在我们实际项目中,我们用到的最多的就是 Vuex 内置的 Logger 插件,它能够帮我们追踪 state 变化,然后输出一些格式化日志。当然我们也可以自己去实现 Vuex 的插件,来帮助我们实现一些特定的需求。