本文解读的Vuex版本为2.3.1
Vuex代码结构
Vuex的代码并不多,但麻雀虽小,五脏俱全,下面来看一下其中的实现细节。
源码分析
入口文件
入口文件src/index.js:
1 | 复制代码import { Store, install } from './store' |
这是Vuex对外暴露的API,其中核心部分是Store,然后是install,它是一个vue插件所必须的方法。Store
和install都在store.js文件中。mapState、mapMutations、mapGetters、mapActions为四个辅助函数,用来将store中的相关属性映射到组件中。
install方法
Vuejs的插件都应该有一个install方法。先看下我们通常使用Vuex的姿势:
1 | 复制代码import Vue from 'vue' |
install方法的源码:
1 | 复制代码export function install (_Vue) { |
方法的入参_Vue就是use的时候传入的Vue构造器。
install方法很简单,先判断下如果Vue已经有值,就抛出错误。这里的Vue是在代码最前面声明的一个内部变量。
1 | 复制代码let Vue // bind on install |
这是为了保证install方法只执行一次。
install方法的最后调用了applyMixin方法。这个方法定义在src/mixin.js中:
1 | 复制代码export default function (Vue) { |
方法判断了一下当前vue的版本,当vue版本>=2的时候,就在Vue上添加了一个全局mixin,要么在init阶段,要么在beforeCreate阶段。Vue上添加的全局mixin会影响到每一个组件。mixin的各种混入方式不同,同名钩子函数将混合为一个数组,因此都将被调用。并且,混合对象的钩子将在组件自身钩子之前。
来看下这个mixin方法vueInit做了些什么:
this.$options用来获取实例的初始化选项,当传入了store的时候,就把这个store挂载到实例的$store上,没有的话,并且实例有parent的,就把parent的$store挂载到当前实例上。这样,我们在Vue的组件中就可以通过this.$store.xxx访问Vuex的各种数据和状态了。
Store构造函数
Vuex中代码最多的就是store.js, 它的构造函数就是Vuex的主体流程。
1 | 复制代码 constructor (options = {}) { |
依然,先来看看使用Store的通常姿势,便于我们知道方法的入参:
1 | 复制代码export default new Vuex.Store({ |
store构造函数的最开始,进行了2个判断。
1 | 复制代码assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) |
这里的assert是util.js里的一个方法。
1 | 复制代码export function assert (condition, msg) { |
先判断一下Vue是否存在,是为了保证在这之前store已经install过了。另外,Vuex依赖Promise,这里也进行了判断。
assert这个函数虽然简单,但这种编程方式值得我们学习。
接着往下看:
1 | 复制代码const { |
这里使用解构并设置默认值的方式来获取传入的值,分别得到了plugins, strict 和state。传入的state也可以是一个方法,方法的返回值作为state。
然后是定义了一些内部变量:
1 | 复制代码// store internal state |
this._committing 表示提交状态,作用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改state。
this._actions 用来存放用户定义的所有的 actions。
this._mutations 用来存放用户定义所有的 mutatins。
this._wrappedGetters 用来存放用户定义的所有 getters。
this._modules 用来存储用户定义的所有modules
this._modulesNamespaceMap 存放module和其namespace的对应关系。
this._subscribers 用来存储所有对 mutation 变化的订阅者。
this._watcherVM 是一个 Vue 对象的实例,主要是利用 Vue 实例方法 $watch 来观测变化的。
这些参数后面会用到,我们再一一展开。
继续往下看:
1 | 复制代码// bind commit and dispatch to self |
如同代码的注释一样,绑定Store类的dispatch和commit方法到当前store实例上。dispatch 和 commit 的实现我们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测所有的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,否则会有一定的性能开销。
构造函数的最后:
1 | 复制代码// init root module. |
Vuex的初始化核心
installModule
使用单一状态树,导致应用的所有状态集中到一个很大的对象。但是,当应用变得很大时,store 对象会变得臃肿不堪。
为了解决以上问题,Vuex 允许我们将 store 分割到模块(module)。每个模块拥有自己的 state、mutation、action、getters、甚至是嵌套子模块——从上至下进行类似的分割。
1 | 复制代码// init root module. |
在进入installModule方法之前,有必要先看下方法的入参this._modules.root是什么。
1 | 复制代码this._modules = new ModuleCollection(options) |
这里主要用到了src/module/module-collection.js 和 src/module/module.js
module-collection.js:
1 | 复制代码export default class ModuleCollection { |
module-collection的构造函数里先定义了实例的root属性,为一个Module实例。然后遍历options里的modules,依次注册。
看下这个Module的构造函数:
1 | 复制代码export default class Module { |
这里的rawModule一层一层的传过来,也就是new Store时候的options。
module实例的_children目前为null,然后设置了实例的_rawModule和state。
回到module-collection构造函数的register方法, 及它用到的相关方法:
1 | 复制代码register (path, rawModule, runtime = true) { |
get方法的入参path为一个数组,例如[‘subModule’, ‘subsubModule’], 这里使用reduce方法,一层一层的取值, this.get(path.slice(0, -1))取到当前module的父module。然后再调用Module类的addChild方法,将改module添加到父module的_children对象上。
然后,如果rawModule上有传入modules的话,就递归一次注册。
看下得到的_modules数据结构:
扯了一大圈,就是为了说明installModule函数的入参,接着回到installModule方法。
1 | 复制代码const isRoot = !path.length |
通过path的length来判断是不是root module。
来看一下getNamespace这个方法:
1 | 复制代码getNamespace (path) { |
又使用reduce方法来累加module的名字。这里的module.namespaced是定义module的时候的参数,例如:
1 | 复制代码export default { |
所以像下面这样定义的store,得到的selectLabelRule的namespace就是’selectLabelRule/‘
1 | 复制代码export default new Vuex.Store({ |
接着看installModule方法:
1 | 复制代码// register in namespace map |
传入了namespaced为true的话,将module根据其namespace放到内部变量_modulesNamespaceMap对象上。
然后
1 | 复制代码// set state |
getNestedState跟前面的getNamespace类似,也是用reduce来获得当前父module的state,最后调用Vue.set将state添加到父module的state上。
看下这里的_withCommit方法:
1 | 复制代码_withCommit (fn) { |
this._committing在Store的构造函数里声明过,初始值为false。这里由于我们是在修改 state,Vuex 中所有对 state 的修改都会用 _withCommit函数包装,保证在同步修改 state 的过程中 this._committing 的值始终为true。这样当我们观测 state 的变化时,如果 this._committing 的值不为 true,则能检查到这个状态修改是有问题的。
看到这里,可能会有点困惑,举个例子来直观感受一下,以 Vuex 源码中的 example/shopping-cart 为例,打开 store/index.js,有这么一段代码:
1 | 复制代码export default new Vuex.Store({ |
这里有两个子 module,cart 和 products,我们打开 store/modules/cart.js,看一下 cart 模块中的 state 定义,代码如下:
1 | 复制代码const state = { |
运行这个项目,打开浏览器,利用 Vue 的调试工具来看一下 Vuex 中的状态,如下图所示:
来看installModule方法的最后:
1 | 复制代码const local = module.context = makeLocalContext(store, namespace, path) |
local为接下来几个方法的入参,我们又要跑偏去看一下makeLocalContext这个方法了:
1 | 复制代码/** |
就像方法的注释所说的,方法用来得到局部的dispatch,commit,getters 和 state, 如果没有namespace的话,就用根store的dispatch, commit等等
以local.dispath为例:
没有namespace为’’的时候,直接使用this.dispatch。有namespace的时候,就在type前加上namespace再dispath。
local参数说完了,接来是分别注册mutation,action和getter。以注册mutation为例说明:
1 | 复制代码module.forEachMutation((mutation, key) => { |
1 | 复制代码function registerMutation (store, type, handler, local) { |
根据mutation的名字找到内部变量_mutations里的数组。然后,将mutation的回到函数push到里面。
例如有这样一个mutation:
1 | 复制代码mutation: { |
就会在_mutations[increment]里放入其回调函数。
commit
前面说到mutation被放到了_mutations对象里。接下来看一下,Store构造函数里最开始的将Store类的dispatch和commit放到当前实例上,那commit一个mutation的执行情况是什么呢?
1 | 复制代码 commit (_type, _payload, _options) { |
方法的最开始用unifyObjectStyle来获取参数,这是因为commit的传参方式有两种:
1 | 复制代码store.commit('increment', { |
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
1 | 复制代码store.commit({ |
1 | 复制代码function unifyObjectStyle (type, payload, options) { |
如果传入的是对象,就做参数转换。
然后判断需要commit的mutation是否注册过了,this._mutations[type],没有就抛错。
然后循环调用_mutations里的每一个mutation回调函数。
然后执行每一个mutation的subscribe回调函数。
Vuex辅助函数
Vuex提供的辅助函数有4个:
以mapGetters为例,看下mapGetters的用法:
代码在src/helpers.js里:
1 | 复制代码export const mapGetters = normalizeNamespace((namespace, getters) => { |
normalizeNamespace方法使用函数式编程的方式,接收一个方法,返回一个方法。
mapGetters接收的参数是一个数组或者一个对象:
1 | 复制代码computed: { |
1 | 复制代码mapGetters({ |
这里是没有传namespace的情况,看下方法的具体实现。
normalizeNamespace开始进行了参数跳转,传入的数组或对象给map,namespace为’’ , 然后执行fn(namespace, map)
接着是normalizeMap方法,返回一个数组,这种形式:
1 | 复制代码{ |
然后往res对象上塞方法,得到如下形式的对象:
1 | 复制代码{ |
也就是最开始mapGetters想要的效果:
完
by kaola/fangwentian
本文转载自: 掘金