1

前言

由于最近在研究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:dispatchcommit
  • 初始化模块收集对象ModuleCollection,并递归收集定义的子模块;
  • 初始化root module(根模块)、并且递归安装子模块,安装过程中将会进行namespaced模块登记、将模块state注入到父级的state中、本地化模块内的getters`mutations`actions,详细将在[installModule]()章节中进行分析;
  • 对store中的state进行响应式处理,并且将getters处理成computed属性
  • 执行plugins

构造函数执行流程

数据模型

Vuex中,存在着三种比较重要的数据结构,这些数据结构构造了整个状态管理,在执行Store构造函数的过程中,ModuleCollection将模块存储在Module中,并构造出一棵模块树,下面就列举一下这三个主要的数据结构StoreModuleCollectionModule

  • Store(仅列举部分)

Store原型

  • ModuleCollections

ModuleCollection原型

  • Module

Module原型

  • 各个模块在注册后,最终会形成类似如下的树状结构:

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使用命名空间对模块间进行隔离,不同命名空间的stateactionsmutationsgetters调用,需要添加模块名,在命名空间内部则不需要声明;
这里需要留意的是,源码中对于actionsmutations的处理,是允许相同命名空间的相同actionsmutations,执行过程时这些actionsmutations都会被处理;

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

改方法将实现Storestate的响应式,并且将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._commitingfalse的时候;
所以在源码中涉及到对state的修改时,都会由_withCommit进行包裹,这是因为_withCommit中会将store._commiting置为true,从而不会抛错;
commit这个API内部的操作,正是有_withCommit进行包裹的;

总结

最后,我们回顾一下开头提出的问题,我们试着来回答一下:

  • Vuex中的state如何做到响应式?

答:是通过内部定义的Vue实例,实现响应式;

  • 命名空间是怎么实现的?

答:命名空间是实现是通过namespaced后,使得stateactionsmutationsgetters的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;

好了,本次的源码分析,就到此结束,希望大家也有所其方法,其中的内容欢迎大家讨论和指正!


Jackie
122 声望4 粉丝