Profession & Efficient

保持对技术的敬畏之心

通俗易懂Vuex源码导读3-Vuex官方文档对照说明

Vuex 的使用解析

回顾前面几章我们介绍的内容

  • 第零章对Vuex的整体运行思路,重点变量进行了介绍。
  • 第一章介绍了Vuex的安装过程。
  • 上一章介绍了Vuex的初始化过程,在正式使用Vuex前做了哪些准备工作。
  • 这一章,将对照Vuex的官方说明文档,逐一介绍示例代码背后的运行逻辑。

首先介绍的是state

  • 唯一状态树
    • 唯一:是指整个Vuex数据存储是由Store这一个对象实例完成的。
    • 状态:是指各个数据。
    • 树:指 module 和 state 的树结构。
    • 通过 Store 变量可以查看当前项目的数据存储情况(作者的用意,可能是将Vuex作为Vue项目的整个数据存储库,将所有数据内容都交由Vuex进行管理),可以通过Store观察整个项目的数据情况。
    • 通过热重载功能,进行数据的回退,实现时间穿梭的调整功能(本系列文章没有介绍到,有兴趣的同学可以看看源码。hot关键字相关内容)。
  • 在计算属性中使用state
    不知你是否会疑惑,在Store的构造函数中并没有定义类的state属性,为什么可以通过store.state获取到state数据呢?

    • state的定义是通过class的取值函数getter及存值函数setter来完成的。取值设值函数的功能,和Object.defineProperties函数类似,相当于设置拦截器,当获取值或设置值时,调用对应函数。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 在vue组件中,调用this.$store.state.count的运行逻辑是:
    • this.$store在「mixin.js」的函数「vuexInit」中定义,在Vuex注册部分介绍过,指向Store对象。
      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • this.\$store.state即调用Store对象的state属性。这时将触发并调用get取值函数,返回this._vm._data.\$$state (这时this指向store对象,因为这是在类中的this指针)

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 相当于调用this.\$store._vm._data.\$\$state,其中_vm在resetStoreVM函数中定义,是一个Vue实例。_vm._data.\$\$state,即这个Vue实例中,通过data定义的一个$$state变量。

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 而这个$$state变量指向的是Store对象的state变量(this._modules.root.state,在构造函数中有介绍),为什么要这样放在Vue中,后续会介绍到。

    • 而根模块的state变量,在模块安装函数(installModule)中,通过递归定义,将各个层级的state变量,都通过树结构组织起来,树的根,或者说树状态的索引入口就是这个根模块的state变量。

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 也就是this.\$store._vm._data.$$state。也就是this.$store.state。

    • 所以Vuex 使用单一状态树,通过State树结构保存了Vuex中的所有数据

    • 为什么state需要通过Vue的data进行保存?

    • 因为我们看到在使用state时,是放在computed中使用, return this.$store.state.count

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 因为要实现数据响应,当state的值发生变化时,需要通知对应的computed函数。

    • 这个功能就要借助vue的数据绑定功能,所以要在data中定义。至于底层原理是什么,请关注后续文章,vue源码解读
    • Vue子组件的注册,是通过minix混合功能来实现,具体原理在「第一章」中介绍过
  • mapState辅助函数

    • 先来看看源码,辅助函数的定义在helpers.js文件中。
    • normalizeNamespace函数,所有辅助函数公用,用于适配「单纯的map写法」以及「带上命名空间的写法」。
    • 「单纯的map写法」是指没有设置命名空间前缀,直接索取需要的变量,如

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 「带上命名空间的写法」即调用 createNamespacedHelpers 返回带上命名空间前缀的辅助函数。即官网介绍的这个

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • createNamespacedHelpers的内部,是将复制函数的第一个参数填写为null,第二个为命名空间前缀

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 而我们看回各辅助函数的定义,是通过normalizeNamespace函数生成的,即相当于往normalizeNamespace函数中,第一个参数填写为null,第二个为命名空间前缀

    • normalizeNamespace函数,是对命名空间前缀的识别和兼容,bind(null)是为了不改变this的指向,让this仍然指向vue组件,参数二是命名空间前缀。这是辅助函数的第一个参数namespace就有了值。

    • 通过bind函数,使得后续传递参数时,后续使用时,从参数的第二个开始填充,此项技术为偏函数
      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 通过偏函数,使得在实际使用时,直接传递所需的属性,与「单纯的map写法」统一用法

    • 下面介绍的是没有「单纯的map写法」的流程,「带上命名空间的写法」如此类推

    • 回调函数参数介绍,namespace是命名空间前缀,states是在调用mapState时,用户传递的内容,可以一个是包含函数,或者键值对的对象,或者一个单纯的key数组。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 定义一个对象res
    • normalizeMap函数,同样所有辅助函数公用,用于兼容数组写法和对象写法。
    • 将内容均解析为key,val对象,例如
    • normalizeMap([a, b, c]) => [ { key: a, val: a }, { key: b, val: b }, { key: c, val: c } ]
    • normalizeMap({a: 1, b: 2, c: 3}) => [ { key: ‘a’, val: 1 }, { key: ‘b’, val: 2 }, { key: ‘c’, val: 3 } ]
    • 这也为什么能兼容多种传值方式

      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 对返回的每个key,val对象,调用回调函数。往res对象中,添加名为上面生成的key的函数

    • 找到state和getters,并判断是否设置命名空间,返回不同的值。有命名空间则通过 getModuleByNamespace 函数返回,
      • 拿到具体模块内部的context变量中的state和getters(实际上也是this.$store.state中的内容,只是添加了命名空间前缀)
    • 兼容函数写法和对象或数组写法,数组或对象,则直接返回值。函数则返回一个绑定了this的函数,并出入state和getters,对应官网,官网的介绍,缺了对参数二getters的介绍,其实如果传入函数时,函数可以接受第二个参数,getter。
    • res[key].vuex = true // 函数标识符,开发中没什么用,在调试工具中使用。
  • 对象展开符
    • 由于返回的是一个res对象,所以可以通过对象展开运算符,展开每一个对象属性。
    • 由于res对象的属性都是一个个函数,所以用在computed中定义计算属性,而不是放在data中定义。

接着介绍的是getter

  • 通过store 的计算属性,例如,this.$store.getters.doneTodosCount

    • this.$store.getters,即store对象的getters属性,坑爹的是,getters也不是在构造函数中定义的。getters在resetStoreVM中定义的。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 通过Object.defineProperty函数,为getters的属性定义拦截器,返回store._vm[key]
    • 而store._vm[key],即调用store内部的vue组件的属性,对应的属性,通过了computed 计算属性去定义,计算属性的函数即为getter函数本身
    • 所以官网介绍「Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)」,其实本身就是计算属性,所以才能将计算结果缓存起来。
  • 通过属性访问,例如,this.$store.getters.doneTodosCount
    • 正如刚刚说的,是在computed定义,当然可以通过对象的方式访问。就好比我们在vue中在computed定义属性,在函数中引用。
  • 通过方法访问,例如,this.$store.getters.getTodoById(2)
    • 即在函数中,返回函数,没有好讲的。
  • mapGetters 辅助函数
    • 大体方法和mapsState类似,同样是同命名空间进行了一些兼容,再同全局getter容器中返回this.$store.getters。

Mutation

  • 示例,store.commit(‘increment’)运行流程
    • 官网中的store,是直接使用Store对象,放在vue组件中,则通过this.$store访问。
    • commit的定义在store的构造函数中,commit函数是绑定了this为store的函数,绑定this是为了在辅助函数使用时,this指针不被改变,前介绍过,bind(null)。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

    • 调用commit函数时,
    • 从_mutations容器中,获取与commit提交的mutation函数同名的数组,即保存同名函数的数组。
    • 调用_withCommit,执行commit时,将状态设置为committing。通过committing标示符,使得其他修改state的都是非法操作。
    • 对数组中的每一个函数进行调用,并传入负载参数,对应官网提交载荷(Payload)
      • 为什么在定义mutation中,还有一个state变量呢?
      • 这因为在注册Mutation函数函数时(registerMutation函数),已经通过call函数,local.state放入了函数的第一个参数中。
    • this._subscribers是在插件中使用的,对每个commit函数的进行监听,订阅 store 的 mutation。handler 会在每个 mutation 完成后调用,该功能常用于插件。
  • 对象风格的提交方式
    • 这个特性是在commit函数第一句被设置的,通过unifyObjectStyle函数兼容对象写法和负载参数写法。
    • unifyObjectStyle函数的原理就是,判断参数是否为对象,是对象则进行解析,并调整参数位置。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

  • Mutation 需遵守 Vue 的响应规则

    • 其实这个跟mutation没什么直接关系,只是说当mutation中使用到state的某属性时,需要提前在state中定义,而不是中往state插入元素,即使插入,也需要通过vue.set插入。
    • 因为store内部,也是通过Vue的date来保存state的。既然想要响应式。自然是需要遵循Vue的规则。
  • 使用常量替代 Mutation 事件类型,这是代码风格问题,与逻辑无关。
  • Mutation 必须是同步函数
    • 因为在store的commit里面,是对mutaion的简单调用,并没有设置回调函数,或者promise resolve。所以必须是同步函数。
  • 在组件中提交 Mutation
    • 大体方法和mapsState类似,同样是同命名空间进行了一些兼容,再同全局_mutation容器中返回,没什么好讲的。

action

  • 函数参数
    • 定义action时,可以传入很多参数。这些参数是从哪里来的呢?
    • 找到registerAction函数,可以看到是在注册Action的时候传递进入的
      《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

module

  • module部分均在介绍模块的配置,属于配置过程,在Vuex的初始化中生效,没有太多的运行逻辑。

总结

  • Vuex本身原理很简单,但是为了模块化,加上了命名空间,添加一堆适配的代码。严重增加了代码复杂度。

    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

工作繁忙,断断续续,历时一个月,终于写完。

  • 希望能大家理解Vuex源码
  • 文章繁琐,不用打我
  • 文章有一定纰漏,欢迎指正
    《通俗易懂Vuex源码导读3-Vuex官方文档对照说明》

各位大佬,觉得OK的话,帮忙点个赞呗~

总目录

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注