Vuex 源码解析(如何阅读源代码实践篇)

5,866 阅读3分钟

上一篇文章说的是如何阅读框架源代码,收到了“如果更详细一点就好了”的反馈,不如就以 Vuex 为切入点进行一次实践吧,不矫揉不造作,说走咱就走~~

一、前提

本文假定你已经对 Vue 的使用上有一定的概念,不要求轻车熟路(使用过 Vuex 当然是最好的),但至少要了解基本的事件绑定方式,以及 Mixin 的用法,官方文档从此去

二、Vuex 解决了什么问题

官方的说法:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
这里首先要搞清楚什么是状态,状态就是数据,也就是说: Vuex 提供了一套 Vue 应用统一的数据源管理模式,除了定义数据源,还定义了数据的管理模式

这其中,Store 所包含的两个核心部分 State 和 Actions 分别代表了数据源,和数据的管理(操作)模式,同时作为一个全局的 VM,其有效的协调了 Vue 各组件间的通信

三、Vuex 的设计思想

如果读 Vue 文档的时候足够留心,兴许你能在插件一节找到蛛丝马迹:

插件的功能包括,通过全局 mixin 方法添加一些组件选项,如:vuex

也就是说,Vuex 不过是 Vue 的一个插件,通过 Mixin 的方式给每个组件注入一个 $store 对象,由于每个组件的 $store 指向的是同一个 store 对象(后面通过详读代码可以知道,这个 $store 其实是一个 VM 对象),所以 store 是全局的,这就印证了之前在我们为什么需要 Vuex中的一个结论,Vuex 类似于一个事件总线

四、详读代码

通过 Mixin 注入 Store

从入口文件 index.js 开始,代码不多,可以直接贴出来

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}

如果你一眼就看出这里的关键是 install,那么你应该领略到读源码先了解设计思想的独特魅力了,没错,作为 Vue 的 Plugin,install 方法就是入口

循着 install 方法进入 store.js,还是符合预期,这个方法主要干得是事情就是 mixin

export function install (_Vue) {
  ...
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

并且还有一个小细节,浏览器环境下并且 Vue 不为空的时候,引入 Vuex 之后是会自动注册的

具体来看看 mixin.js 这个文件,划重点(注意看注释):

// 通过钩子 init / beforeCreate 执行 vuexInit
const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })

// 组件初始化的时候注入 $store
function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
}

Store 对象

Vuex 的最佳实践中,一般这样使用(带着目标去阅读,效果更佳):

// create store
const store = new Vuex.Store({
  actions: {
    ...
  },
  modules: {
    ...
  }
})
import App from './comps/app.vue'
new Vue(Vue.util.extend({ el: '#root', store }, App))

我们需要新建一个 Store,在创建 Vue 实例的时候,作为参数传入,在上一节的 vuexInit 函数中,是从 this.$options 中取出 store 赋值给组件的 $store 的,如此,便能无缝联系上了

接下来的重点,就是 Store 这个类了,还是 store.js 这个文件,怀着入参为 ations 和 modules 的预期,来读 constructor 方法,倒是有一个语句是用来处理 modules 的

this._modules = new ModuleCollection(options)

但真的是寻寻觅觅寻不到从 options 中取出 actions 进行处理的方法,当然后面仔细阅读了 ModuleCollection 中的代码之后,才找到了答案,actions 参数也是在这里面提取的。毕竟让我纠结迷茫了良久,如果是我来写的话,我可能不会这么写,方法的命名需要有语义性,而且一个方法也应当只做一件事情

原则上为了尽快理清主流程,有些细节需要暂时略过(所以语义化的命名、合理的函数拆分,对阅读者来说是多么的重要),假设已经知道前面的步骤已经从 options 中读到了 actions 和 modules,那么下一个核心节点就是:

installModule(this, state, [], this._modules.root)

这一步再进行分解(注意看注释)

  // 注册 mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 注册 action
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  // 注册 getter (computed)
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 遍历子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })

出于篇幅以及希望阅读的同学亲自实践的目的,具体的注册方式这里不再展开

进入下一个重要环节 resetStoreVM,创建 VM,实现数据监听(注意看注释)

function resetStoreVM (store, state, hot) {

  // bind store public getters
  // getters 其实就是 computed
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 创建一个 Vue 实例,作为 Store 的 VM
  store._vm = new Vue({
    data: {
      ?state: state
    },
    computed
  })
  ...
}

五、小结

至此,Vuex 的主流程代码基本上算是走了一遍,看似神奇,可是代码量并不大,还是那句话,希望阅读的同学能够按照这个套路自己走一遍

本文在公众号菲麦前端同步发行: