Vuex(一)

    Vuex 是 专为 Vue.js 开发的状态管理模式,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可以预测的方式发生变化。

    在开发过程中,我们经常遇到 多个视图依赖 同一个数据的情况,且不说 如果是 子孙组件 之间的 多层跨越式 传参非常繁琐,对于 兄弟组件 之间的数据传递更是无能为力。因此,Vuex 就诞生了,它是一个 全局单例模式 的状态管理,我们的组件无论在什么地方,都能获取状态或者出发行为。


    Vuex 核心思想

        Vuex 应用的核心就是 store(仓库)。store 就是⼀个容器,它包含着你的应用中大部分的状态(state)。

        那么我们也可以定义一个全局的对象来挂在数据,比如 window,为什么需要使用 Vuex ?

        那么 Vuex 和单纯的全局对象有以下 两点 不同:

        1 Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变化,那么 相应的组件也会相应地得到高效更新

        2 你不能直接改变 store 中的状态。改变 store 中的状态的 唯⼀途径 就是 显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每⼀个状态的变化,从而让我们能够实现⼀些工具帮助我们更好地了解我们的应用。


    Vuex 初始化

        当我们引用 Vuex 的时候,其实是引用了一个 对象。

import Vuex from "vuex";

    和 Vue-Router ⼀样,Vuex 也同样存在⼀个静态的 install 方法。

    install 的逻辑很简单,把传入的 _Vue 赋值给 Vue 并执行了 applyMixin(Vue) 方法。

applyMixin

    其实就 全局混⼊了⼀个 beforeCreated 钩子函数,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例,这也是为什么我们在组件中可以通过 this.$store 访问到这个实例


实例化

    我们在 import Vuex 之后,会实例化其中的 Store 对象,返回 store 实例并传入 new Vue 的options 中,也就是我们刚才提到的 options.store。

使用vuex
返回 store 实例并传入 new Vue 的options 中

    Store 对象的构造函数接收⼀个对象参数,它包含actions 、 getters 、 state 、 mutations 、 modules 等 Vuex 的核心概念, Store 的实例化过程分为 三个部分:初始化模块,安装模块 和 初始化 store._vm

 Store 构造函数

初始化模块

    在 Vue 开发过程中,我们秉承组件化开发的理念,将页面分成了若干个组件,自然的,各个组件维护者自己的 state。Vuex 是一颗 单一状态树,所有的 state 都会集中在 store 中,那么为了更好的区分各个组件所对应在 store 中的位置,我们也可以同样组件化的区分 store,这样,我们的组件 和 store 就能更好的对应,方便我们自己查找和开发。

    Vuex 允许我们将 store 划分为 模块(module),每个模块就是一个小的 store,每个模块拥有自己的state 、 mutation 、 action 、 getter ,甚⾄是嵌套子模块——从上至下进行同样方式的分割, 这就像我们常说的 麻雀虽小五脏俱全。

使用

    模块 是一个 树形 的数据结构, store 可以理解为一个 root module,下面的就是 子模块,Vuex 要完成构建这棵树:

    this._modules = new ModuleCollection(options);

ModuleCollection

    实例化 ModuleCollection 的过程就是 执行了自己的 register 方法,register 接收 3 个参数,path 表示路径,我们整体目标是要构建⼀颗模块树, path 是在构建树的过程中维护的路径; rawModule 表示定义模块的原始配置; runtime 表示是否是⼀个运行时创建的模块。

    register 内部首先 var newModule = new Module(rawModule, runtime);

    Module 是 用来描述 单个模块的类。其代码

Module

    对于每个模块 this._rawModule = rawModule, 表示 每个模块的配置,this.state 表示每个模块定义的 state, this._children 表示 它的所有子模块。

    register 实例化 Module 后,判断当前路径  path 的长度,首次调用的时候 this.register([], rawRootModule, false), 传入的是 空数组,则说明它是 根模块,this.root = newModule,赋值给 root;

    const store = new Vuex.Store({

        modules: {

            a: moduleA,

            b: moduleB

    }})

    我们的 path 就 是 a 和 b

    否则就要生成 父子关系

     var parent = this.get(path.slice(0, -1));

     parent.addChild(path[path.length - 1], newModule);

    最后也是最关键的,判断是否有 子模块,递归 调用 register 生成树

    if (rawModule.modules) {

      forEachValue(rawModule.modules, function (rawChildModule, key) {

        this$1.register(path.concat(key), rawChildModule, runtime);

      });

    }

    传入的 path 是 父模块的 path(我们将 modules 这个 object 的 key 作为 path), 通过 this.get(path.slice(0, -1) 方法,找到对应的模块,然后 addChild 建立父子关系。

     root module 的下⼀层 modules ,它们的 parent 就是 root module ,他们会被添加的 root module 的 _children 中。每个⼦模块通过路径找到它的⽗模块,然后通过⽗模块的 addChild ⽅法建立父子关系,递归执行这样的过程,最终就建⽴⼀颗完整的模块树。


安装模块

    初始化模块之后我们得到了一个树状的 store 仓库,接下来就是 执行安装模块的相关逻辑,它的目标就是对模块中的 state 、 getters 、 mutations 、 actions 做初始化工作

    给我们 在

     this._actions = Object.create(null);

      this._actionSubscribers = [];

      this._mutations = Object.create(null);

      this._wrappedGetters = Object.create(null);

      定义的这些数据初始化,其实就是 遍历这个树状结构,将每个小模块中的 actions、mutations、getters 集中在一起,建立我们的数据仓库,这样方便我们在使用。

结果

    代码入口

      this._modules = new ModuleCollection(options);

      var state = this._modules.root.state;

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

installModule

      然后就是核心的 installModule 方法, installModule  有 5 个参数, store 表示 root store ; state 表示 rootstate ; path 表示模块的访问路径; module 表示当前的模块, hot 表示是否是热更新。

        首先是命名空间的概念,默认情况下,模块内部的 action 、 mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同⼀ mutation 或 action 作出响应。

        如果我们希望模块具有更⾼的封装度和复⽤性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter 、 action 及 mutation 都会⾃动根据模块注册的路径调整命名。就像上面结果图片显示一样, 会带上 modules 的 key: 比如调用方法 commit['a/changeloading']。

        1 首先根据 path 获取 namespace

        比如 module a 和  module b 中 都有一个方法名为 changeItem 的方法,方法名字相同,比如 在 module a 或者 b 的内部,actions 中调用 mutation 都是 commit(‘changeItem’), 我们并没有写成commit(‘a/changeItem’),那么外部是如何区分它的呢,就是 makeLocalContext 它帮我们拼接了。

        对于 getters 而言,如果没有 namespace ,则直接返回 root store 的 getters ,否则返回makeLocalGetters(store, namespace) 的返回值

        2 接着   var local = module.context = makeLocalContext(store, namespace, path), 它的作用是 构建一个本地上下文环境

        makeLocalContext ⽀持 3 个参数相关, store 表示 root store ; namespace 表示模块的命名空间, path 表示模块的 path 。该⽅法定义了 local 对象,对于 dispatch 和 commit 方法,如果没有 namespace ,它们就直接指向了 root store 的 dispatch 和 commit ⽅法,否则会创建⽅法,把 type(就是我们 dispatch 和 commit 的方法名 ) ⾃动拼接上namespace(比如 commit(‘a/changeItem’), 就是 namespace/type ) ,然后执⾏ store 上对应的方法。

makeLocalContext

        它重构了 dispatch 和 commit、getters, 如果 namespace 为 true 的话,就包装一下 commit,将 commit 的参数 加上我们的 namespace 的,这样就明确指定了我们所要调用的方法所在的模块。

    3 遍历 mutation、action、getter

        我们包装好 上下文环境之后就要遍历 该模块中的 mutation、action、getter,比如

mutation
是个数组

      ⾸先遍历模块中的 mutations 的定义,拿到每⼀个 mutation 和 key ,并把 key 拼接上namespace ,然后执⾏ registerMutation ⽅法。该⽅法实际上就是给 root store 上的_mutations[types] 添加 wrappedMutationHandler ⽅法。注意,同一type 的 _mutations 可以对应多个方法。

        最后还是 递归调用 installModule, 为 它的 子模块 进行注册。

         module.forEachChild(function (child, key) {

            installModule(store, rootState, path.concat(key), child, hot);

          });

    之前我们忽略了⾮ root module 下的 state 初始化逻辑,之前我们提到过 getNestedState ⽅法,它是从 root state 开始,⼀层层根据模块名能访问到对应 path 的 state ,那么它每⼀层关系的建⽴实际上就是通过这段 state 的初始化逻辑。

state
最后构成的 state

初始化 store._vm

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

         resetStoreVM(this, state);

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

    resetStoreVM 首先遍历了 _wrappedGetters 获得每个 getter 的函数 fn 和 key ,然后定义了 computed[key] = () => fn(store) 。

    fn(store) 相当于执⾏如下⽅法:

         store._wrappedGetters[type] = function wrappedGetter (store) {

            return rawGetter(

              local.state, // local state

              local.getters, // local getters

              store.state, // root state

              store.getters // root getters

        )

      } ;

      rawGetter 就是我们在执行  registerGetter(store, namespacedType, getter, local) 中传入的是用户定义的 getter 函数,它的前 2 个参数是 local state 和 local getters ,后 2 个参数是 root state 和 root getters 。

    那么  computed[key] 就获取了 我们所有的 getters。

    接着就是 实例化一个 Vue 实例 store._vm ,并把 computed 传⼊:

    store._vm = new Vue({

        data: {

            $$state: state

        },

        computed

    })

    我们发现 data 选项⾥定义了 $$state 属性,⽽我们访问 store.state 的时候,实际上会访问Store 类上定义的 state 的 get ⽅法:

    Object.defineProperties( Store.prototype, prototypeAccessors$1 );

    var prototypeAccessors$1 = { state: { configurable: true } };

    prototypeAccessors$1.state.get = function () {

      return this._vm._data.$$state

    };

    它实际上就访问了 store._vm_data.$$state,那么 getters 和 state 如何建⽴依赖逻辑:

    forEachValue(wrappedGetters, (fn, key) => {

        computed[key] = () => fn(store)

        Object.defineProperty(store.getters, key, {

            get: () => store._vm[key],

            enumerable: true // for local getters

        })

    })

    当我根据 key 访问 store.getters 的某⼀个 getter 的时候,实际上就是访问了store._vm[key] ,也就是 computed[key] ,在执行 computed[key] 对应的函数的时候,会执行rawGetter(local.state,...) 方法,那么就会访问到 store.state ,进而访问到store._vm_data.$$state ,这样就建立了⼀个依赖关系。当 store.state 发⽣变化的时候,下⼀次再访问 store.getters 的时候会重新计算。

    再来看⼀下 strict mode 的逻辑

    当严格模式下, store._vm 会添加⼀个 wathcer 来观测 this._data.$$state 的变化,也就是当store.state 被修改的时候, store._committing 必须为 true,否则在开发阶段会报警告。 store._committing 默认值是 false ,那么它什么时候会 true 呢, Store 定义了_withCommit 实例⽅法:

它就是对 fn 包装了⼀个环境,确保在 fn 中执⾏任何逻辑的时候 this._committing = true 。

所以外部任何⾮通过 Vuex 提供的接口直接操作修改 state 的⾏为都会在开发阶段触发警告。


总结

    学到 vuex 的知识点,比如 初始化模块中 如何构建一个树结构, for 循环 遍历子元素(广度), 递归 生成深度。

    为了代码的维护性和可读性,将 modules 分成小 module,然后通过初始化、安装模块 和 建立 getters 和 state 的联系。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • 导航 [深入01] 执行上下文[https://juejin.im/post/684490404605093479...
    woow_wu7阅读 225评论 0 0
  • 1.前言 状态管理在开发中已经算是老生常谈了,本篇文章我们转向前端方向的Vue框架,看看Vuex是怎么通过stor...
    bigcatduan阅读 4,161评论 0 2
  • vuex官方文档 Vuex是什么? Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存...
    yutao618阅读 3,194评论 0 3
  • Vuex源码阅读分析 Vuex是专为Vue开发的统一状态管理工具。当我们的项目不是很复杂时,一些交互可以通过全局事...
    steinslin阅读 633评论 0 6
  • 前言 之前几篇解析 Vue 源码的文章都是完整的分析整个源码的执行过程,这篇文章我会将重点放在核心原理的解析,不会...
    心_c2a2阅读 1,466评论 1 8