[Vue.js进阶]从源码角度剖析 Vuex

image

前言

以前几篇解析 Vue 源码的文章都是完整的分析整个源码的执行过程,这篇文章我会将重点放在核心原理的解析javascript

完整源码地址vue

有兴趣的朋友也能够看我学习源码时的详细注释 源码地址java

Vuex 版本:3.1.0git

Vuex 简介

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,通俗的来讲就是将本来分散在各个组件的数据,经过一个公共的仓库存储,使得每一个组件都能直接从 Vuex 中获取数据,能够把它想象成一个全局变量,可是和全局变量不一样的是github

  1. Vuex 状态存储是响应式的,当 Vuex 中的状态发生改变,会通知全部依赖到的组件更新数据
  2. 强调状态的可预测,可追踪,因此严格模式没法直接从 Vuex 中修改状态,必须经过提交 mutation 同步修改

当某些数据可能会发生变化,而且被多个不一样的组件依赖时,能够考虑将数据放到 Vuex 中存储,例如表格每页显示的最大条数vue-router

将最大页数存储在 state 中,一旦用户修改最大页数,须要反映到全部分页器组件,这时只需派发一个 mutation 修改对应的 state 便可vuex

使用 Vuex

使用 Vuex 分为 3 步api

  1. 安装 Vuex 插件
  2. 实例化 Vuex 的仓库 Store
  3. 将第二步的实例传入根 Vue 实例中

安装 Vuex 插件核心原理和 vue-router 相同,调用插件暴露的 install 方法,经过 Vue.mixin 全局混入 beforeCreate 钩子,以后每当初始化一个组件,都会生成一个 $store 属性指向根 Vue 实例中的 store 对象,最后全部的组件均可以经过 this.$store 访问根实例中的 store 对象数组

当咱们执行 new Vuex.Store 就会建立一个仓库实例 store闭包

以后将第二步生成的实例注入根 Vue 实例

实例化 Store

Vuex 全部的行为都是围绕 new Vuex.Store 生成的 store 实例展开的,在实例化 Store 的过程当中,主要作了三件事

  1. 初始化模块
  2. 安装模块
  3. 建立一个管理全部数据的 Vue 实例

初始化模块

咱们知道,Vuex 是支持模块嵌套的,即在一个 Vuex 模块内部,能够经过 modules 属性嵌套子模块,从而造成一个树形的结构,经过模块的划分能够在复杂的状况更好的管理模块,Vuex 将这个树形结构的模块保存在 store 实例的 _modules 属性中

this._modules = new ModuleCollection(options)
复制代码

image.png

ModuleCollection 的实例表明了全部模块的集合,即这个树形结构,我称之为模块树,它在实例化时会调用 register 方法,注册全部模块

rawModule 即 new Vuex.Store 传入的模块配置项,包括根模块在内,每一个模块都是 Module 的一个实例,将第一次调用 register 方法传入的模块做为 root 根模块,以后会遍历 modules 对象,递归调用 register 注册子模块

同时子模块会经过 get 方法找到父模块,并经过 addChild 往父模块的 _children 属性添加当前子模块,从而创建父子关系

这里有个很是重要的参数,即 path ,它是一个数组,第一次调用 register 时, path 是一个空数组,每当递归调用时,会将 path 拼接当前子模块的属性名,举个例子

export default new Vuex.Store({
  // 根模块
  modules: {
    // 子模块A
    moduleA: {
      actions: {
        action(context) {context.commit('mutation')},
      },
      mutations: {
        mutation() {}
      },
      
      modules: {
        // 孙子模块B
        moduleB: {
          actions: {
            action(context) {context.commit('mutation')},
          },
          mutations: {
            mutation() {}
          },
        }
        
      }
    },
  }
})
复制代码

在子模块 moduleA中,path 的值为 ["moduleA"],而对于孙子模块 moduleB,path 的值为 ["moduleA,"moduleB"],有了这样的层级关系,就能够经过 path 数组很好的找到对应的模块

安装模块

安装模块和初始化模块的区别在于,初始化模块会创建整个模块树(ModuleCollection ),而安装模块会给模块添加做用于每一个模块的 dispatch,mutation,getters 的 context 对象

什么意思呢,以上图为例,当咱们在一个 action 中触发一个 mutation 时,通常会经过 action 第一个参数 context 的 commit 属性来触发

可是若是别的模块也存在名为 mutation 的 mutation ,此时就会发生冲突, Vuex 为了解决这个问题引入了命名空间的概念,引用官网的一句话

若是但愿你的模块具备更高的封装度和复用性,你能够经过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的全部 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

当设置 namespaced:true 的模块,其 context 参数中的 commit 只会影响到当前模块下的 mutations,实现方法其实很是的简单:执行 context .commit 最终会给 mutation 拼上模块的命名前缀再执行全局对应的 commit

若是模块没有设置 namespaced 则使用全局的 store.commit,不然会拼上 namespace 再调用全局的 store.commit,而 namespace 是根据以前介绍到的模块的 path 数组生成的命名前缀

getNamespace 会经过 reduce 遍历 path 数组,递归向下遍历子模块,当子模块设置了 namespaced 时会给 namespace 变量拼接当前模块名

因此当 mutation 拼上模块的命名前缀就不会发生冲突,结合以前的例子,由于子模块 moduleA 中的 path 值为 ["moduleA"],因此 mutation 最终会变为 moduleA/mutation ,而孙子模块 moduleB 中的 path 值为 ["moduleA","moduleB"], mutation 最终会变为 moduleA/moduleB/mutation

对于 context.actions 和 context.getters 实现大体的思路也是相同,最后会递归的给模块树(ModuleCollection )的全部子模块生成 context 对象

carbon.png

建立 Vue 实例

之因此 Vuex 中的状态在发生变化时可以通知到全部依赖的组件,是由于 Vuex 在 Store 实例中建立了一个内部的 Vue 实例用来管理全部的状态

根模块的 state 表明了整个 Vuex 的全部数据, Vuex 将根模块的 state 做为 $$state 属性的值保存在内部 Vue 实例中,同时将 wrappedGetters (在安装模块时,会将全部的 getters 保存在这个对象中)中的全部的 getter 做为 computed 属性

经过 Vue 响应式原理能够知道,若是组件经过 this.$store.<prop 名> 依赖到了 Vuex 的某个数据,当 $$state 中的任何状态发生变化,都会触发内部的 setter 函数, 从而通知依赖到的组件发生视图更新

这里再介绍一下 Vuex 中的 getters,它们最终都会变成内部 Vue 实例的 computed 属性, 当某个 getter 依赖的值发生变化会触发从新计算,从而执行 fn(store) 这个函数,store 是的 Store 实例,而 fn 又是什么呢?在安装模块时,会定义 store._wrappedGetters 这个对象,fn 就是 wrappedGetter 这个函数

根据官方文档能够发现,每一个 getter 支持 4 个参数,当前模块的 state,当前模块的 getters,全局的 state,全局的 getters,对应 rawGetter 的 4 个参数(rawGetter 即开发者定义的 getter 函数)

Vuex 经过返回一个函数,使其保存了 local(context )对象,又经过传入参数使得可以访问全局的 store 实例,很是灵活的运用了闭包

Vuex 核心 api

Vuex 容许开发者经过 dispatch 派发一个异步的 action,经过 commit 提交一个同步的 mutation

之因此区分异步和同步是为了可以更加准确的追踪状态的变化,由于就像没法准确知道一个响应什么时候会收到同样,异步操做并不能准确的知道什么时候修改的数据,因此不能将修改 state 的操做放在 action 中,可是咱们能够在异步完成后经过提交一个 commit 的形式同步的修改 state ,同步的特色使得任何状态的变化都可以确切知道执行先后 state 的状态,以便完成一些高级操做, 例如记录日志,时间旅行等

dispatch

在安装模块中给模块添加做用于每一个模块的 dispatch 时,会给每一个 action 包裹一层函数,做用是保证每一个 action 都是一个 Promise

而 store 实例的 dispatch 方法会经过 Promise 的 then 方法解析 action ,当存在同名的 action ( 多个模块含有相同命名的 action 且没有使用命名空间),会使用 Promise.all 并发的解析

commit

经过 commit 方法能够同步的执行一个 mutation,以前提到,在严格模式下 Vuex 规定只有 mutation 才能同步修改数据,由于这样才能方便数据追踪,Vuex 声明了一个 _withCommit 方法,只有调用这个方法才能修改 state,相似一个开关的功能,当执行一个 mutation 时,会调用它使得容许修改 state

至于只有调用 _withCommit 方法才能修改 state 的原理也很简单,由于 state 都被保存在内部 Vue 实例中,经过 Vue 的 $watch 深度监听整个 state 当发现 _committing 为 false 就发出警告

在根模块设置 strict 为 true 开启严格模式时才会启用检查,多是考虑到深度监听影响性能,因此推荐只在开发环境启用

其余 API 原理

Vuex 还提供了不少其余的 API ,涉及到篇幅缘由这里简要介绍下内部实现原理

map 系列的辅助函数

在组件中经过 mapState ,mapActions,mapMutations,mapGetters 辅助函数,能够省去写 this.$store.<prop 名> ,直接使用 this.<prop 名> 这种写法,同时让项目分层更加清晰,也是比较推荐的写法,这些辅助函数最终都会返回一个对象,因此须要使用 ES9 的对象扩展运算符将对象放入对应的 Vue 属性中

同时这些 map 辅助函数能够经过传入多个参数来实现命名空间的功能

核心原理是将传入的第一个参数,也就是命名前缀拼上对应的 state 名(action / mutation / getter 名),去 store 实例中 _modulesNamespaceMap 属性中找到对应模块(Module 实例),由于在安装模块的过程当中会给每一个模块添加 context 属性,因此这里就能够经过 context 对象拿到做用于当前模块的 state (action / mutation / getter )

至于 _modulesNamespaceMap 是在以前安装模块时生成的,保存了每一个模块和对应的命名前缀

拿到 context 对象后,根据不一样的功能返回不一样的对象给组件

  • state:返回指定模块内部的 state,若是是一个函数就传入 store 实例返回执行后的结果
  • action:返回指定模块 context.dispatch,执行 action 会拼上命名前缀执行 store 的 dispatch
  • mutation: 同 action
  • getter:访问 getter 会拼上命名前缀访问 store.getters 对象对应的 getter

plugins

Vuex 自身也提供了一个插件功能,用于监听 action 和 mutation

原理是采用了观察者模式,声明一个订阅者数组,每当执行完一个 action / mutation 都会遍历数组中全部订阅者依次执行回调,同时回调中还会传入 action / mutation 名和当前的 state 状态,而插件只须要调用 store.subscribeAction / store.subscribe 将订阅者放入数组中便可

// 调用订阅者的回调函数
 this._subscribers.forEach(sub => sub(mutation, this.state))
复制代码

replaceState

根据传入的参数替换 Vuex 中的 state,Vuex 使用这个 API 实现时光旅行的功能

原理也很简单,只需经过 _withCommit 修改内部 Vue 实例中保存全部状态的 $$state 属性便可,虽然时光旅行只能在开发模式中使用,可是咱们能够将它抽象出来,开发一个 plugin 记录每一个 mutation 提交时的状态(须要深拷贝)和步骤,调用 replaceState 使数据回滚到指定步骤中

registerModule

Vuex 还提供了动态注册模块的功能,经过传入模块和模块插入的位置,来动态注入到已有的模块树中

介绍模块树 ModuleCollection 时提到它有一个 register 方法,经过传入的 path 数组和模块,插入到模块树中对应的位置,正好对应 registerModule 的 2 个参数,而 registerModule 在将模块插入到整个模块树以后,还会给传入的模块执行安装模块的函数,以及重置 Vue 实例

由于全部的数据都会保存在 store.state 中,因此重置 Vue 实例并不会致使丢失以前的数据

参考资料

Vue.js 技术揭秘

相关文章
相关标签/搜索