Vuex 源码学习梳理

334 阅读6分钟

vuex

1、introduction

Vuex 是一个专门为 Vue.js 应用程序开发的状态管理模式。

它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化

什么是“状态管理模式”?

  • 1、state, 驱动应用的数据源
  • 2、view, 以声明方式将state映射到视图
  • 3、actions, 响应在view上的用户输入导致的状态变化

Vuex 核心思想

  • 1、Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含这应用中大部分的状态(state)
  • 2、Vuex 的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应的得到高效的更新。
  • 3、不能直接改变store中的状态,改变store中的状态的唯一途径就是显式的提交(commit)mutation。 这样使得我们可以方便的跟踪每一个状态的变化。从而让我们能够实现一些工具帮助我们更好的了解我们的应用

2、Vuex初始化

1、安装

  • 1、当我们在代码中通过import Vuex from 'vuex' 的时候,实际上引入的是一个对象, 定义在src/index.js中:

    export default {
      Store,
      install,
      version: '__VERSION__',
      mapState,
      mapMutations,
      mapGetters,
      mapActions,
      createNamespacedHelpers
    }
    
  • 2、Vuex 存在一个静态的install 方法,定义在 src/store.js中

    export function install (_Vue) {
      if (Vue && _Vue === Vue) { // 先判断Vue是否已经被赋值,避免二次安装
        if (process.env.NODE_ENV !== 'production') {
          console.error(
            '[vuex] already installed. Vue.use(Vuex) should be called only once.'
          )
        }
        return
      }
      Vue = _Vue
      applyMixin(Vue)
    }
    
  • 3、install 的逻辑很简单,把传入的_Vue 赋值给Vue 并执行了 applyMixin(Vue) 方法,执行

    Vue.mixin({beforeCreate: vuexInit})
    

    其实就是给Vue 全局混入了一个beforeCreate 钩子函数,它的实现非常简单,就是把options.store 保存在所有组件的this.$store 中。这个options.store 就是我们在实例化Store 对象的实例。

2、Store 实例化

  • 1、用法

    export default new Vuex.Store({
      modules: {
        modulesA,
        mouduleB
      },
      state: {
        count: 0
      },
      getters: { //...},
      actions: { //...},
      mutations: { //...}
      }
    })
    

    Store 对象的构造函数也是一个Class, 接收一个对象参数,它包含 modules\state\getters\actions\mutations等Vuex 的核心概念

  • 2、初始化模块

    • 1、Vuex 允许我们将store 分割成模块(module)每个模块拥有自己的state\mutation\getter\action 甚至是嵌套子模块——从上至下进行同样的方式分割

      const modulesA = {
        state: {
          count: 'A'
        }
      }
      const mouduleB = {
        state: {
          count: 'B'
        }
      }
      
      export default new Vuex.Store({
        modules: {
          modulesA,
          mouduleB
        }
      })
      

      从数据结构上来看,模块的设计就是一个树形结构,store本身可以理解为一个root module, 它下面的modules就是子模块,Vuex 需要完成这颗树的构建

    • 2、构建的入口

       this._modules = new ModuleCollection(options) 
      

      调用register方法,通过const newModule = new Module(rawModule, runtime)创建了一个Module实例。Module是用来描述单个模块的类

       export default class ModuleCollection {
         constructor (rawRootModule) {
           // 注册 root module (Vuex.Store options)
           this.register([], rawRootModule, false)
         }
         // ...
          register (path, rawModule, runtime = true) {
           if (process.env.NODE_ENV !== 'production') {
             // 断言 rawModule中的getters、actions、mutations必须为指定的类型
             assertRawModule(path, rawModule)
           }
       
           // 实例化一个module
           const newModule = new Module(rawModule, runtime)
           if (path.length === 0) {
             // 如果path为0,就把当前module 作为根module 绑定到root属性上
             this.root = newModule
           } else {
             // 子module 添加其父module的_children属性上
             const parent = this.get(path.slice(0, -1))
             parent.addChild(path[path.length - 1], newModule)
           }
       
           // 如果当前模块存在子模块(modules字段)
           // 遍历子模块,逐个注册,最终形成一个树
           if (rawModule.modules) {  // 判断是否定义了module ,然后遍历,递归执行register
             forEachValue(rawModule.modules, (rawChildModule, key) => {
               this.register(path.concat(key), rawChildModule, runtime)
             })
           }
         }
       }
      

      1、 register 首先根据路径获取到父模块,然后再调用父模块的addchild方法建立父子关系
      2、 register的最后一步,就是遍历当前模块定义中的所有modules, 根据key 作为 path, 递归调用 register 方法,这样就建立了父子关系

  • 3、安装模块

    对模块中的state、getters、actions、mutations 做初始化工作

    • 入口:

      const state = this._modules.root.state
      installModule(this, state, [], this._modules.root)
      
      • 1、默认情况下,模块内部的action、mutation、getter 是注册在全局命名空间的——这样多个模块能够对同一mutation 或 action作出响应。如果我们希望模块具有更高的封装度和复用性,可以通过添加 namespaced:true 的方式使其成为带有命名空间的模块。当模块被注册后,它的所有 action、mutation、getter 都会自动根据模块注册的路径调整命名

      • 2、构造了一个本地上下文环境

        const local = module.context = makeLocalContext(store, namespace, path)
        
      • 3、registerMutation

      • 4、registerAction

      • 5、registerGetter

      • 总结

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

  • 4、初始化 store._vm

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

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

      • strict mode

        • 当严格模式下,store._vm 会添加一个watcher 来观测this._data.?state 的改变,也就是当 store.state 被修改的时候,store._committing 必须为true ,否则在开发阶段会报警告

               if (store.strict) {
                  enableStrictMode(store)
                }
              function enableStrictMode (store) {
                store._vm.$watch(function () { return this._data.$$state }, () => {
                  if (process.env.NODE_ENV !== 'production') {
                    // 不允许在mutation之外修改state
                    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
                  }
                }, { deep: true, sync: true })
              }
          

3、总结

  • 我们要把store想象成一个数据仓库,为了更方便的管理,我们把一个大的store 拆分成一些modules, 整个modules 是一个树形结构,每个module 有分别定义了state\getters\mutations\actions, 我们也通过递归遍历模块的方式都完成了他们的初始化,为了module 具有更高的封装度和复用性,还定义了namespace 的概念,最后我们还定义了一个内部的Vue实例,用来建立state 到getters 的联系,并且可以在严格模式下监测state的变化是不是来自外部,确保改变state 的唯一途径就是显式的提交mutation

3、API

1、数据获取

  • Vuex 最终 存储的数据是在state上的,我们之前分析过 在 store.state 存储的是root state, 那么对于模块上的state, 假设我们有2个嵌套的modules, 他们的key 分别为a 和 b, 我们可以通过store.state.a/b.XXX 的方式获取

    • 在递归执行installModule 的过程中,就完成了整个state的建设,这样我们就可以通过module 名的path 去访问到一个深层module的state
  • 获取

    • state

      • 当我们使用store.state获取数据

        get state() {
         return this._vm._data.$$state
        }
        // 安装
        resetStoreVm(this, state)
        

        做了什么?:

         store._vm = new Vue({
                   data: {
                          $$state: state
                     },
                   computed
               })
        

        实例化一个Vue, 然后绑定到store._vm上

    • getters

        store.getters = {}
        const wrappedGetters = store._wrappedGetters
        const computed = {}
        forEachValue(wrappedGetters, (fn, key) => {
                computed[key] = () => fn(store)
                Object.defineProperty(store.getters, key, {
                       get: () => store._vm[key],
                        enumerable: true 
                  })
        })
      

2、数据存储

  • 1、Vuex 对数据存储的本质上就是对state的修改并且只允许我们通过提交mutation的形式去修改state。

  • 2、mutation 必须是同步函数

  • 3、action

    • action 类似于mutaion, 不同在于action提交的是mutation 而不是直接操作 state, 并且它可以包含任意的异步操作

      • action 比我们自己写一个异步操作然后提交mutation 的好处: 它可以在参数中获取到当前模块的一些方法和状态,Vuex帮我们做好了这些

3、语法糖

  • 1、mapState

    • mapState 支持传入 namespace, 因为我们可以这样写:

      computed:{
         ...mapState['some/name/module',{
            a:state => state.a
            b:state => state.b
          }]
      }
      
    • 在mapState 的实现中,如果有namespace, 则去尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace) 对应的module, 然后把state和getters 修改为module对应的state和getters

    • 主要原因是在Vuex 初始化执行 installModule的过程中,初始化了这个映射表:

            ```
            function installModule (store, rootState, path, module, hot) {
              const isRoot = !path.length
              const namespace = store._modules.getNamespace(path)
            
              // register in namespace map
              if (module.namespaced) {
                // // 保存namespaced模块
                store._modulesNamespaceMap[namespace] = module
              }
            }
            ```
      
  • 2、mapGetters

    • mapGetters 的用法:

      import {mapGetters} from 'vuex'
      
      export default {
        computed:{
         ...mapGetters[{
            someGetter
            // ...
         }]
        }
      }
      
    • 和mapState类似,mapGetters 是将 store 中的getter 映射到局部计算属性

    • mapGetters 也同样支持 namespace, 如果不写namespace, 访问一个子module 的属性需要写很长key,一旦我们使用了namespace, 就可以方便我们的书写,每个mappedGetter 的实现,实际上就是取 this.$store.getters[val]

  • 3、mapMutations

    • 我们可以在组件中使用 this.$store.commit['xxx'] 提交mutation, 或者使用mapMutations 辅助函数将组件中的methods 映射为store.commit 的调用。 mapMutations 支持传入一个数组或者一个对象,目标都是组件中对应的methods 映射为store.commit的调用。

      • 用法:

        import { mapMutations } from 'vuex'
        
        export default {
          // ...
          methods: {
            ...mapMutations([
              'increment', // map `this.increment()` to `this.$store.commit('increment')`
        
              // `mapMutations` also supports payloads:
              'incrementBy' // map `this.incrementBy(amount)` to `this.$store.commit('incrementBy', amount)`
            ]),
            ...mapMutations({
              add: 'increment' // map `this.add()` to `this.$store.commit('increment')`
            })
          }
        }
        
    • mappedMutation 同样支持了namespace, 并且支持了传入额外的参数args, 作为提交mutation的payload, 最终就是执行了store.commit方法,并且这个 commit 会根据传入namespace 映射到对应 module的commit上。

  • 4、mapActions

    • 在组件中使用 this.$store.dispatch['xxx'] 提交action, 或者使用mapActions 辅助函数将组件中的methods 映射为 store.dispatch的调用

    • mapActions 在用法上和mapMutations 几乎一样,实现也很类似。和mapMutations 实现几乎一样,不同的就是把commit 方法换成了dispatch

4、动态更新模块

  • 1、模块动态注册 registerModule

    • 在有一些场景下,我们需要动态取注入一些新的模块,Vuex 提供了模块动态注册的功能,在store 上提供了一个registerModule 的Api

       registerModule (path, rawModule, options = {}) {
          if (typeof path === 'string') path = [path]
      
          if (process.env.NODE_ENV !== 'production') {
            assert(Array.isArray(path), `module path must be a string or an Array.`)
            assert(path.length > 0, 'cannot register the root module by using registerModule.')
          }
      
          this._modules.register(path, rawModule)
          installModule(this, this.state, path, this._modules.get(path), options.preserveState)
          // reset store to update getters...
          resetStoreVM(this, this.state)
        }
      
    • registerModule 支持传入一个path模块路径和rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行installModule 去安装模块,最后执行 resetStoreVM重新实例化 store._vm, 并销毁旧的store._vm

  • 2、动态卸载模块 unregisterModule

    • 相对的,Vuex 提供了公开动态卸载功能,在store 上提供了一个unregisterModule 的Api

      unregisterModule (path) {
          if (typeof path === 'string') path = [path]
      
          if (process.env.NODE_ENV !== 'production') {
            assert(Array.isArray(path), `module path must be a string or an Array.`)
          }
      
          this._modules.unregister(path)
          this._withCommit(() => {
            const parentState = getNestedState(this.state, path.slice(0, -1))
            Vue.delete(parentState, path[path.length - 1])
          })
          resetStore(this)
        }
      
    • 1、unregisterModule 支持传入一个path模块路径,首先执行unregister 方法去修减我们的模块树。注意,这里只会移除我们运行时动态创建的模块

    • 2、 接着 会删除 state 在该路径下的引用,最后执行 resetStore方法。

    • 3、 该方法就是把 store 下的对应存储的_actions、_mutations、_wrappedGetters 和 _modulesNamespaceMap 都清空,然后重新执行 installModule 安装所有模块以及 resetStoreVM 重置 store._vm

4、插件

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

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

    const {
       plugins = [],
       strict = false
    } = options
    plugins.forEach(plugin => plugin(this))
    
  • 2、Logger 插件

    • 1、 Logger 函数,它相当于订阅了mutation的提交, 它的 prevState 表示之前的 state, nextState 表示提交 mutation 后的state, 这两个 state 都需要执行 deepCopy 方法拷贝一份对象的副本,这样对他们的修改就不会影响原始的store.state

    • 2、 接下来就是构造一些格式化的消息,打印出一些时间 消息 message, 之前的状态 prevState, 对应的 mutation 操作 formattedMutation 以及下一个状态 nextState

    • 3、 最后跟新 prevState = nextState, 为下一次提交 mutation 输出日志做准备

    • 4、总结

      Vuex 从设计上支持了插件,让我们很好的从外部追踪 store 内部的变化, Logger 插件在我们的开发阶段也提供了很好的指引作用。