• 深入理解Vuex 框架


    Vuex是一个专为Vue服务,用于管理页面数据状态、提供统一数据操作的生态系统。它集中于MVC模式中的Model层,规定所有的数据操作必须通过 action – mutation – state change 的流程来进行,再结合Vue的数据视图双向绑定特性来实现页面的展示更新。统一的页面状态管理以及操作处理,可以让复杂的组件交互变得简单清晰,同时可在调试模式下进行时光机般的倒退前进操作,查看数据改变过程,使code debug更加方便。

    最近在开发的项目中用到了Vuex来管理整体页面状态,遇到了很多问题。决定研究下源码,在答疑解惑之外,能深入学习其实现原理。

    先将问题抛出来,使学习和研究更有针对性:

    1. 使用Vuex只需执行 Vue.use(Vuex),并在Vue的配置中传入一个store对象的示例,store是如何实现注入的?
    2. state内部是如何实现支持模块配置和模块嵌套的?
    3. 在执行dispatch触发action(commit同理)的时候,只需传入(type, payload),action执行函数中第一个参数store从哪里获取的?
    4. 如何区分state是外部直接修改,还是通过mutation方法修改的?
    5. 调试时的“时空穿梭”功能是如何实现的?

    注:本文对有Vuex有实际使用经验的同学帮助更大,能更清晰理解Vuex的工作流程和原理,使用起来更得心应手。初次接触的同学,可以先参考Vuex官方文档进行基础概念的学习。

    一、框架核心流程

    进行源码分析之前,先了解一下官方文档中提供的核心思想图,它也代表着整个Vuex框架的运行流程。
    vuex-core
    如图示,Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。围绕这个生态圈,简要介绍一下各模块在核心流程中的主要功能:

    • Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
    • dispatch:操作行为触发方法,是唯一能执行action的方法。
    • actions:操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
    • commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
    • mutations:状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
    • state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
    • getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

    Vue组件接收交互行为,调用dispatch方法触发action相关处理,若页面状态需要改变,则调用commit方法提交mutation修改state,通过getters获取到state新值,重新渲染Vue Components,界面随之更新。

    二、目录结构介绍

    打开Vuex项目,看下源码目录结构。

    dir_structure

    Vuex提供了非常强大的状态管理功能,源码代码量却不多,目录结构划分也很清晰。先大体介绍下各个目录文件的功能:

    • module:提供module对象与module对象树的创建功能;
    • plugins:提供开发辅助插件,如“时光穿梭”功能,state修改的日志记录功能等;
    • helpers.js:提供action、mutations以及getters的查找API;
    • index.js:是源码主入口文件,提供store的各module构建安装;
    • mixin.js:提供了store在Vue实例上的装载注入;
    • util.js:提供了工具方法如find、deepCopy、forEachValue以及assert等方法。

    三、初始化装载与注入

    了解大概的目录及对应功能后,下面开始进行源码分析。index.js中包含了所有的核心代码,从该文件入手进行分析。

    3.1 装载实例

    先看个简单的例子:

    store.js文件中,加载Vuex框架,创建并导出一个空配置的store对象实例。

    然后在index.js中,正常初始化一个页面根级别的Vue组件,传入这个自定义的store对象。

    问题1所述,以上实例除了Vue的初始化代码,只是多了一个store对象的传入。一起看下源码中的实现方式。

    3.2 装载分析

    index.js文件代码执行开头,定义局部 Vue 变量,用于判断是否已经装载和减少全局作用域查找。

    然后判断若处于浏览器环境下且加载过Vue,则执行install方法。

    install方法将Vuex装载到Vue对象上,Vue.use(Vuex) 也是通过它执行,先看下Vue.use方法实现:

    若是首次加载,将局部Vue变量赋值为全局的Vue对象,并执行applyMixin方法,install实现如下:

    来看下applyMixin方法内部代码。如果是2.x.x以上版本,可以使用 hook 的形式进行注入,或使用封装并替换Vue对象原型的_init方法,实现注入。

    具体实现:将初始化Vue根组件时传入的store设置到this对象的$store属性上,子组件从其父组件引用$store属性,层层嵌套进行设置。在任意组件中执行 this.$store 都能找到装载的那个store对象,vuexInit方法实现如下:

    看个图例理解下store的传递。

    页面Vue结构图:
    cart_vue_structure

    对应store流向:
    cart_vue_structure

    四、store对象构造

    上面对Vuex框架的装载以及注入自定义store对象进行分析,解决了问题1。接下来详细分析store对象的内部功能和具体实现,来解答 为什么actions、getters、mutations中能从arguments[0]中拿到store的相关数据? 等问题。

    store对象实现逻辑比较复杂,先看下构造方法的整体逻辑流程来帮助后面的理解:

    cart_vue_structure

    4.1 环境判断

    开始分析store的构造函数,分小节逐函数逐行的分析其功能。

    在store构造函数中执行环境判断,以下都是Vuex工作的必要条件:

    1. 已经执行安装函数进行装载;
    2. 支持Promise语法。

    assert函数是一个简单的断言函数的实现,一行代码即可实现。

    4.2 数据初始化、module树构造

    环境判断后,根据new构造传入的options或默认值,初始化内部数据。

    调用 new Vuex.store(options) 时传入的options对象,用于构造ModuleCollection类,下面看看其功能。

    ModuleCollection主要将传入的options对象整个构造为一个module对象,并循环调用 this.register([key], rawModule, false) 为其中的modules属性进行模块注册,使其都成为module对象,最后options对象被构造成一个完整的组件树。ModuleCollection类还提供了modules的更替功能,详细实现可以查看源文件module-collection.js

    4.3 dispatch与commit设置

    继续回到store的构造函数代码。

    封装替换原型中的dispatch和commit方法,将this指向当前store对象。dispatch和commit方法具体实现如下:

    前面提到,dispatch的功能是触发并传递一些参数(payload)给对应type的action。因为其支持2种调用方法,所以在dispatch中,先进行参数的适配处理,然后判断action type是否存在,若存在就逐个执行(注:上面代码中的this._actions[type] 以及 下面的 this._mutations[type] 均是处理过的函数集合,具体内容留到后面进行分析)。

    commit方法和dispatch相比虽然都是触发type,但是对应的处理却相对复杂,代码如下。

    该方法同样支持2种调用方法。先进行参数适配,判断触发mutation type,利用_withCommit方法执行本次批量触发mutation处理函数,并传入payload参数。执行完成后,通知所有_subscribers(订阅函数)本次操作的mutation对象以及当前的state状态,如果传入了已经移除的silent选项则进行提示警告。

    4.4 state修改方法

    _withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。实现代码如下。

    缓存执行时的committing状态将当前状态设置为true后进行本次提交操作,待操作完毕后,将committing状态还原为之前的状态。

    4.5 module安装

    绑定dispatch和commit方法之后,进行严格模式的设置,以及模块的安装(installModule)。由于占用资源较多影响页面性能,严格模式建议只在开发模式开启,上线后需要关闭。

    4.5.1 初始化rootState

    上述代码的备注中,提到installModule方法初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中,让我们看看其中的代码实现。

    判断是否是根目录,以及是否设置了命名空间,若存在则在namespace中进行module的存储,在不是根组件且不是 hot 条件的情况下,通过getNestedState方法拿到该module父级的state,拿到其所在的 moduleName ,调用 Vue.set(parentState, moduleName, module.state) 方法将其state设置到父级state对象的moduleName属性中,由此实现该模块的state注册(首次执行这里,因为是根目录注册,所以并不会执行该条件中的方法)。getNestedState方法代码很简单,分析path拿到state,如下。

    4.5.2 module上下文环境设置

    命名空间和根目录条件判断完毕后,接下来定义local变量和module.context的值,执行makeLocalContext方法,为该module设置局部的 dispatch、commit方法以及getters和state(由于namespace的存在需要做兼容处理)。

    4.5.3 mutations、actions以及getters注册

    定义local环境后,循环注册我们在options中配置的action以及mutation等。逐个分析各注册函数之前,先看下模块间的逻辑关系流程图:

    complete_flow

    下面分析代码逻辑:

    registerMutation方法中,获取store中的对应mutation type的处理函数集合,将新的处理函数push进去。这里将我们设置在mutations type上对应的 handler 进行了封装,给原函数传入了state。在执行 commit(‘xxx’, payload) 的时候,type为 xxx 的mutation的所有handler都会接收到state以及payload,这就是在handler里面拿到state的原因。

    action和getter的注册也是同理的,看一下代码(注:前面提到的 this.actions 以及 this.mutations在此处进行设置)。

    action handler比mutation handler以及getter wrapper多拿到dispatch和commit操作方法,因此action可以进行dispatch action和commit mutation操作。

    4.5.4 子module安装

    注册完了根组件的actions、mutations以及getters后,递归调用自身,为子组件注册其state,actions、mutations以及getters等。

    4.5.5 实例结合

    前面介绍了dispatch和commit方法以及actions等的实现,下面结合一个官方的购物车实例中的部分代码来加深理解。

    Vuex配置代码:

    Vuex组件module中各模块state配置代码部分:

    加载上述配置后,页面state结构如下图:

    cart_state

    state中的属性配置都是按照option配置中module path的规则来进行的,下面看action的操作实例。

    Vuecart组件代码部分:

    Vuexcart.js组件action配置代码部分:

    Vue组件中点击购买执行当前module的dispatch方法,传入type值为 ‘checkout’,payload值为 ‘products’,在源码中dispatch方法在所有注册过的actions中查找’checkout’的对应执行数组,取出循环执行。执行的是被封装过的被命名为wrappedActionHandler的方法,真正传入的checkout的执行函数在wrappedActionHandler这个方法中被执行,源码如下(注:前面贴过,这里再看一次):

    handler在这里就是传入的checkout函数,其执行需要的commit以及state就是在这里被传入,payload也传入了,在实例中对应接收的参数名为products。commit的执行也是同理的,实例中checkout还进行了一次commit操作,提交一次type值为types.CHECKOUT_REQUEST的修改,因为mutation名字是唯一的,这里进行了常量形式的调用,防止命名重复,执行跟源码分析中一致,调用 function wrappedMutationHandler (payload) { handler(local.state, payload) } 封装函数来实际调用配置的mutation方法。

    看到完源码分析和上面的小实例,应该能理解dispatch action和commit mutation的工作原理了。接着看源码,看看getters是如何实现state实时访问的。

    4.6 store._vm组件设置

    执行完各module的install后,执行resetStoreVM方法,进行store组件的初始化。

    综合前面的分析可以了解到,Vuex其实构建的就是一个名为store的vm组件,所有配置的state、actions、mutations以及getters都是其组件的属性,所有的操作都是对这个vm组件进行的。

    一起看下resetStoreVM方法的内部实现。

    resetStoreVm方法创建了当前store实例的_vm组件,至此store就创建完毕了。上面代码涉及到了严格模式的判断,看一下严格模式如何实现的。

    很简单的应用,监视state的变化,如果没有通过 this._withCommit() 方法进行state修改,则报错。

    4.7 plugin注入

    最后执行plugin的植入。

    devtoolPlugin提供的功能有3个:

    源码分析到这里,Vuex框架的实现原理基本都已经分析完毕。

    五、总结

    最后我们回过来看文章开始提出的5个问题。

    1.  使用Vuex只需执行 Vue.use(Vuex),并在Vue的配置中传入一个store对象的示例,store是如何实现注入的?

    :Vue.use(Vuex) 方法执行的是install方法,它实现了Vue实例对象的init方法封装和注入,使传入的store对象被设置到Vue上下文环境的$store中。因此在Vue Component任意地方都能够通过this.$store访问到该store。

    2.  state内部支持模块配置和模块嵌套,如何实现的?

    :在store构造方法中有makeLocalContext方法,所有module都会有一个local context,根据配置时的path进行匹配。所以执行如dispatch(‘submitOrder’, payload)这类action时,默认的拿到都是module的local state,如果要访问最外层或者是其他module的state,只能从rootState按照path路径逐步进行访问。

    3.  在执行dispatch触发action(commit同理)的时候,只需传入(type, payload),action执行函数中第一个参数store从哪里获取的?

    :store初始化时,所有配置的action和mutation以及getters均被封装过。在执行如dispatch(‘submitOrder’, payload)的时候,actions中type为submitOrder的所有处理方法都是被封装后的,其第一个参数为当前的store对象,所以能够获取到 { dispatch, commit, state, rootState } 等数据。

    4.  Vuex如何区分state是外部直接修改,还是通过mutation方法修改的?

    :Vuex中修改state的唯一渠道就是执行 commit(‘xx’, payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing标志变量为true,然后才能修改state,修改完毕还需要还原_committing变量。外部修改虽然能够直接修改state,但是并没有修改_committing标志位,所以只要watch一下state,state change时判断是否_committing值为true,即可判断修改的合法性。

    5.  调试时的”时空穿梭”功能是如何实现的?

    :devtoolPlugin中提供了此功能。因为dev模式下所有的state change都会被记录下来,’时空穿梭’ 功能其实就是将当前的state替换为记录中某个时刻的state状态,利用 store.replaceState(targetState) 方法将执行this._vm.state = state 实现。

    源码中还有一些工具函数类似registerModule、unregisterModule、hotUpdate、watch以及subscribe等,如有兴趣可以打开源码看看,这里不再细述。

  • 相关阅读:
    父子组件双向通讯
    高德地图采坑实践之文本标记点击事件左半边不生效(已解决)
    高德地图 location字段控制台显示 为字符串类型 实际为对象
    允许长单词、数字、URL换行到下一行
    python 格式化输出详解(占位符:%、format、f表达式)——上篇 理论篇
    python tkinter实现俄罗斯方块 基础版
    python递归三战:Sierpinski Triangle、Tower of Hanoi、Maze Exploring
    python(pygame)滑稽大战(类似飞机大战) 教程
    编辑器测试-TinyMCE
    封装cookie localStorage sessionStorage
  • 原文地址:https://www.cnblogs.com/Ponlai/p/9967050.html
Copyright © 2020-2023  润新知