【Vuex 源码学习】第六篇 - Vuex 的模块收集

news2024/9/29 7:25:49

一,前言

上一篇,主要介绍了 Vuex 中 Mutations 和 Actions 的实现,主要涉及以下几个点:

  • 将 options 选项中定义的 mutation 方法绑定到 store 实例的 mutations 对象;
  • 创建并实现 commit 方法(同步);
  • 将 options 选项中定义的 action 方法绑定到 store 实例的 actions 对象;
  • 创建并实现 dispatch 方法(异步);

至此,一个简易版的 Vuex 状态管理插件就完成了;
(尚不支持模块、命名空间、插件等高级能力及扩展能力)

本篇,继续介绍 Vuex 模块相关概念:Vuex 模块收集的实现;


二,Vuex 模块的概念

前面分别介绍了 vuex 中 state、getters、mutations、actions 的实现;

当项目庞大且状态复杂时,state、getters、mutations、actions 就会格外混乱甚至难以维护;

这时,我们希望能够将他们拆分为独立的模块(即作用域);每个模块为单独文件进行维护;

在每个独立的模块下,包含当前模块的全部状态:state、getters、mutations、actions;

定义模块,需要使用 Vuex 提供的 modules 属性;

当 Vuex 初始化时进行模块加载,会将 modules 中声明的多个模块与根模块进行合并;

Vuex 的模块,理论上是支持无限层级递归的模块树;

备注:

  • 当多个模块中,存在相同名称的状态时,默认会同时变化;可添加 namespaced 命名空间进行隔离;
  • 命名相同的模块会在模块收集阶段被覆盖;

三,Vuex 模块的使用

1,创建 Demo

基于之前示例代码的设计,为了完整地测试 vuex 的 modules 模块与 namespaced 命名空间功能;

仿造当前 Demo,另外再创建 2 个相似的模块:moduleA、moduleB:

模块 A:moduleA,修改 state.name = 20;

// src/store/moduleA

export default {
  state: {
    num: 20
  },
  getters: {
  },
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  actions: {
  }
};

模块 B:moduleB,修改 state.name = 30;

// src/store/moduleB

export default {
  state: {
    num: 30
  },
  getters: {
  },
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  actions: {
  }
};

在 src/store/index.js 中,引入并通过 modules 属性注册 moduleA 和 moduleB 两个模块:(即将 A、B 两个模块注册成为 index 根模块下的子模块)

// src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';  // 使用 vuex 官方插件进行功能测试

// 引入两个测试模块
import moduleA from './moduleA'
import moduleB from './moduleB'

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    //...
  },
  getters: {
    //...
  },
  mutations: {
    //...
  },
  actions: {
    //...
  },
  // 在根模块下注册子模块 A、B
  modules:{
    moduleA,
    moduleB
  }
});

export default store;

在 src/App.vue 中,测试模块方法的调用:

获取模块中的状态:this.$store.state.moduleA.num

备注:操作子模块时,需要添加对应的模块路径;

// src/App.vue

<template>
  <div id="app">
    商品数量: {{this.$store.state.num}} 个<br>
    商品单价: 10 元<br>
    订单金额: {{this.$store.getters.getPrice}} 元<br>
    <button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>
    <button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>
    <!-- 测试 State 数据响应式 -->
    <button @click="$store.state.num = 100">测试 State 数据响应式</button>

    <br> 模块测试: <br>
    A 模块-商品数量: {{this.$store.state.moduleA.num}} 个<br>
    B 模块-商品数量: {{this.$store.state.moduleB.num}} 个<br>
  </div>
</template>

npm run serve 启动服务,测试模块状态取值:

操作子模块

2,发现问题

测试状态更新:点击同步更新按钮

同步更新

测试结果:
三个模块(根模块、模块 A、模块 B)中的三个同名状态 num 会同时发生改变,并触发了视图更新;

3,问题分析

从表象上看,虽然通过 modules 进行了模块拆分,但模块间的状态仍不是独立的;

备注:由于 demo 示例的特殊性,多个模块存在相同名称的属性:商品数量 num;

当 Vuex 初始化时,将进行模块合并,多个模块中的相同状态会被合并为一个数组;

之后,当通过同步更新方法 $store.commit('changeNum',5) 进行状态提交时,Vuex 会找到所有的changeNum 方法并依次执行,这就将导致同名属性一起更新;

为了使模块间的状态独立,即产生独立的作用域,需要通过 namespaced 命名空间 ,在状态合并阶段进行隔离;

4,命名空间

想要严格划分一个空间,需要为模块再添加 namespaced 命名空间:

// src/store/moduleA

export default {
  namespaced: true,	// 启动命名空间
  state: {
    num: 20
  },
  getters: {
  },
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  actions: {
  }
};

添加了 namespaced 命名空间后,再点击更新按钮,模块中的状态不会发生改变:

备注:这时,触发对应模块的数据更新需要添加模块的命名空间标识

<template>
  <div id="app">
    <br> 模块测试: <br>
    A 模块-商品数量: {{this.$store.state.moduleA.num}} 个<br>
    B 模块-商品数量: {{this.$store.state.moduleB.num}} 个<br>
    <button @click="$store.commit('moduleA/changeNum',5)">A 模块-同步更新:数量+5</button>
    <button @click="$store.commit('moduleB/changeNum',5)">B 模块-同步更新:数量+5</button>
  </div>
</template>

测试效果:

添加命名空间后


四,Vuex 模块收集的实现

1,模块的树型结构-模块树

文中第二部分提到:Vuex 的模块,理论上是支持无限层级递归的树型结构;

但当前版本的 Vuex 仅仅处理了单层 options 对象;

因此,需要继续添加支持深层处理的递归逻辑,从而完成“模块树”的构建,即:实现 Vuex 的模块收集;

为了能够模拟出多层级的“模块树”,我们再创建一个模块 ModuleC,并注册到 ModuleA 下;

模块层级设计:index 根模块中包含模块 A、模块 B;模块 A 中,又包含了模块 C;

模块 C:moduleC,修改 state.name = 40;

export default {
  namespaced: true,
  state: {
    num: 40
  },
  getters: {
  },
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  actions: {
  }
};

在模块 A 中引入模块 C,并将模块 C 注册为模块 A 的子模块:

import moduleC from './moduleC'

export default {
  namespaced: true,
  state: {
    num: 20
  },
  getters: {
  },
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  actions: {
  },
  modules: {
    moduleC
  }
};

2,模块收集的逻辑

将 options 数据进行格式化,添加父子模块的层级关系,构建成为“模块树”;

创建 src/vuex/module/module-collection.js,用于对 options 进行格式化处理,即:通过递归地将子模块注册到对应的父模块上,完成“模块树”的构建;

备注:Vuex 的模块收集过程与 Vue 源码中 AST 语法树的构建过程相似:

  • 首先,层级上都有父子关系,且理论上支持无限递归;
  • 其次,都采用了深度优先遍历,需使用栈保存层级关系(这里的栈相当于地图的作用);

当 Vuex 模块收集完成后,我们期望的构建结果如下:

// 模块树对象
{
  _raw: '根模块',
  _children:{
    moduleA:{
      _raw:"模块A",
      _children:{
        moduleC:{
          _raw:"模块C",
          _children:{},
          state:'模块C的状态'  
        }
    	},
    	state:'模块A的状态'  
    },
    moduleB:{
      _raw:"模块B",
      _children:{},
      state:'模块B的状态'  
    }
  },
  state:'根模块的状态'
}

3,模块收集的实现

在 src/vuex 目录下,创建 module 模块目录,并创建 module-collection.js 文件,创建 ModuleCollection 类:用于在 Vuex 初始化时进行模块收集操作;

/**
 * 模块收集操作
 *  处理用户传入的 options 选项
 *  将子模块注册到对应的父模块上
 */
class ModuleCollection {
  constructor(options) {
    // ...
  }
}

export default ModuleCollection;

模块收集的操作,就是(深度优先)递归地处理 options 选项中的 modules 模块,构建成为树型结构:

创建 register 方法:携带模块路径,对当前模块进行注册,执行“模块树”对象的构建逻辑;

class ModuleCollection {
  constructor(options) {
    // 从根模块开始,将子模块注册到父模块中
    // 参数1数组:栈结构,用于存储路径,标识模块树的层级关系
    this.register([], options);
  }
  /**
   * 将子模块注册到父模块中
   * @param {*} path       数组类型,当前待注册模块的完整路径
   * @param {*} rootModule 当前待注册模块对象
   */
  register(path, rootModule) {
    // 格式化,并将当前模块,注册到对应的父模块上
  }
}

export default ModuleCollection;

1,处理根模块

从根模块开始构建:格式化根模块,并初始化 newModule “模块树”对象 :

class ModuleCollection {
  constructor(options) {
    this.register([], options);
  }
  register(path, rootModule) {
    // 格式化:构建 Module 对象
    let newModule = {
      _raw: rootModule,        // 当前模块的完整对象
      _children: {},           // 当前模块的子模块
      state: rootModule.state  // 当前模块的状态
    }

    // 根模块时:创建模块树的根对象
    if (path.length == 0) {
      this.root = newModule;
    } else {
      // 非根模块时:将当前模块,注册到对应父模块上
    }
  }
}

export default ModuleCollection;

若当前模块存在 modules 子模块,递归调用 register 方法(深度优先),继续注册子模块:

class ModuleCollection {
  constructor(options) {
    this.register([], options);
  }
  register(path, rootModule) {
    let newModule = {
      _raw: rootModule,
      _children: {},
      state: rootModule.state
    }

    if (path.length == 0) {
      this.root = newModule;
    } else {
      // 非根模块时:将当前模块,注册到对应父模块上
    }

    // 若当前模块存在子模块,继续注册子模块
    if (rootModule.modules) {
      // 采用深度递归方式处理子模块
      Object.keys(rootModule.modules).forEach(moduleName => {
        let module = rootModule.modules[moduleName];
        // 将子模块注册到对应的父模块上
        // 1,path:待注册子模块的完整路径,当前父模块path拼接子模块名moduleName
        // 2,module:当前待注册子模块对象
        this.register(path.concat(moduleName), module)
      });
    }
  }
}

export default ModuleCollection;

注意,调用 register 时,需要拼接好当前子模块的路径层级,便于确定层级关系时,快速查找父模块;

2,处理子模块

当 register 方法处理非根模块时,需要将当前模块,注册到对应父模块上;

这就需要从 root 模块树对象中,逐层地查找到当前模块的父模块对象,并将子模块添加进去:

class ModuleCollection {
  constructor(options) {
    this.register([], options);
  }
  register(path, rootModule) {
    let newModule = {
      _raw: rootModule,
      _children: {},
      state: rootModule.state
    }

    if (path.length == 0) {
      this.root = newModule;
    // 非根模块时:将当前模块,注册到对应父模块上
    } else {
      // 逐层找到当前模块的父亲(例如:path = [a,b,c,d])
      let parent = path.slice(0, -1).reduce((memo, current) => {
        //从根模块中找到a模块;从a模块中找到b模块;从b模块中找到c模块;结束返回c模块即为d模块的父亲
        return memo._children[current];
      }, this.root)
      // 将d模块注册到c模块上
      parent._children[path[path.length - 1]] = newModule
    }
    
    if (rootModule.modules) {
      Object.keys(rootModule.modules).forEach(moduleName => {
        let module = rootModule.modules[moduleName];
        this.register(path.concat(moduleName), module)
      });
    }
  }
}

export default ModuleCollection;

3,子模块注册逻辑

假设:模块层级深度为[a, b, c, d];

问:如何将模块 d 注册到它的父模块 c 上?

  • 根据 register 方法中模块 d 携带的模块路径 path,即 path = [a, b, c, d]
  • 通过path.slice(0, -1).reduce 逐层地从当前已经构建完成的 root 模块树对象上,找到模块d 的父模块,即模块 c;
  • 将子模块 d 格式化后,注册到父模块 c 上,这样就完成了 Vuex 的模块收集操作;

测试模块树的构建结果:

在根模块 root 对象中,包含两个子模块:模块 A 和模块 B;

其中,模块 A 包含一个子模块:模块 C;

构建结果与期望相符,模块树构建完成;


五,结尾

本篇,主要介绍了 vuex 模块收集是如何实现的,主要包括以下几点:

  • Vuex 模块的概念;
  • Vuex 模块和命名空间的使用;
  • Vuex 模块收集的实现-构建“模块树”;

下一篇,继续介绍 Vuex 模块相关概念:Vuex 模块安装的实现;


维护日志

  • 20211006
    • 对部分描述进行调整,使模块收集思路及关键逻辑通俗易懂;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/339238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最近挺火的人工智能chatGPT注册

文章目录1.前提预备1.1 短信接收平台1.2 ip加速&#xff0c;不做说明2.注册chatGPT步骤2.1 进入chat.openai.com网址后&#xff0c;点击sign up2.2 可以使用qq邮箱注册2.3 填写好邮箱&#xff0c;然后点击Continue,然后再填写密码2.4 之后在qq邮箱进行验证注册(注意&#xff1a…

C++入门——内存管理

C入门——内存管理 C/C内存分布 分类是为了更好的管理 int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVar 1;int localVar 1;int num1[10] {1, 2, 3, 4};char char2[] "abcd";char* pChar3 "abcd";int* ptr1 (…

Java、JSP环境保护与宣传网站的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;本文对环境保护与宣传网站的设计和开发过程进行了详细地分析与叙述。按照系统开发的实际操作流程以及论文编写的规范&#xff0c;论文内容从系统概述、系统分析、系统设计和系统实现这四大模块对系统的开发过程分别进行了阐述。系统…

python3-API流量回放/锲约测试/自动化测试

PPL-Tester 简介 http工具集,通过代理获取到API的请求与响应信息,将这些请求信息进行流量回放/锲约测试或快速生成用例, 亦可通过人工进行修改参数化提取、变量引用、断言等形成API自动化测试用例等! 你以为只是流量回放吗?错~走去瞧瞧v2版本! 看官~请记得给个star呗? 项…

驱动 | Linux | NVMe - 1. 概述

本文主要参考2篇相关的解析 1’ 2 和 linux 源码 3。 此处推荐一个可以便捷查看 linux 源码的网站 bootlin 4。 更新&#xff1a;2022 / 02 / 11 驱动 | Linux | NVMe - 1. 概述与nvme_core_init函数解析NVMe 的前世今生NVMe CommandPCI 总线从架构角度看 NVMe 驱动NVMe 驱动的…

前端开发中如何处理接口数据过大的问题

题引&#xff1a; 当我们在公司做项目的时候&#xff0c;难免会遇到后端接口直接给你返回成千上万的数据进行渲染。如果我们直接一股脑遍历添加的话&#xff0c;就会导致空白页面的等待时间是很长且异常卡顿&#xff0c;那么对于数据过大的渲染就需要进行特殊的处理。这也是一…

PyQt5数据库开发1 4.1 SQL Server 2008 R2如何开启数据库的远程连接

文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服务器 4 设置sa用户属性 5 配置服务器 6 重新登录 7 配置SSCM 8 确认防火墙设置 注意事项 前言 SQL Server 2008 R2如何开启数据库的远程连接 SQL Server 2008默认是不允许远程连…

ExecutorService、Callable、Future实现有返回结果的多线程原理解析

在并发多线程场景下&#xff0c;存在需要获取各线程的异步执行结果&#xff0c;这时&#xff0c;就可以通过ExecutorService线程池结合Callable、Future来实现。 我们先来写一个简单的例子—— public class ExecutorTest {public static void main(String[] args) throws Ex…

KMP 算法

1 应用场景-字符串匹配问题  字符串匹配问题&#xff1a;&#xff1a; 有一个字符串 str1 ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””&#xff0c;和一个子串 str2“尚硅谷你尚硅 你” 2) 现在要判断 str1 是否含有 str2, 如果存在&#xff0c;就返回第一次出现…

数据与C(limits.h数据常数介绍)

本章简单的介绍一下limits.h的数据常量&#xff0c;这里简单了解一下就好了 目录 一.limits.h 二.float.h头文件 一.limits.h CHAR_BIT char类型的位数 CHARMAX char类型的最大值 CHAR_MIN char类型的最小值 SCHAR_MAX signed char类型的最大…

SpringBoot图片上传和访问路径映射

图片上传和静态资源映射编写controller层接口上传到文件夹相关配置1 application.properties配置文件&#xff1a;2 Constant类&#xff1a;文件的资源映射配置WebMvcConfigurer的继承类注意测试编写controller层接口 ApiOperation("图片上传功能")PostMapping(&quo…

Java笔记-volatile和AtomicInteger

目录1. volatile1.1.什么是volatile1.2.JMM-Java内存模型2 验证volatile的特性2.1 可见性2.2.验证volatile不保证原子性2.3 volatile实现禁止指令重排序3.使用AtomicInteger解决volatile的不能实现原子性的问题3.2 AtomicInteger的方法说明&#xff1a;3.3 CAS3.4 应用1. volat…

linux-进程1-进程概述

写在最前 记录一下linux的进程学习专题 1. 程序和进程的区别 1.1 程序 程序是包含一系列信息的文件&#xff0c;这些信息描述了如何在运行时创建一个进程&#xff1a; 二进制格式标识&#xff1a;每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解 释文…

Redis实战-session共享之修改登录拦截器

在上一篇中Redis实战之session共享&#xff0c;我们知道了通过Redis实现session共享了&#xff0c;那么token怎么续命呢&#xff1f;怎么刷新用户呢&#xff1f;本来咱们就通过拦截器来实现这两个功能。 登录拦截器优化&#xff1a; 先来看看现在拦截器情况&#xff1a; 拦截…

JavaScipt基础学习(1)

1. JavaScript特点 JavaScript是脚本编写语言&#xff1b;所有主流浏览器都支持JavaScript&#xff1b;JavaScript基于对象语言&#xff1b;JavaScriptb变量类型是弱类型&#xff0c;没有如Java一样严格的数据类型&#xff1b;变量是弱类型的。因此定义变量时&#xff0c;只使…

WindowsServer服务器系列:部署FTP文件服务

1、点击“开始”菜单&#xff0c;选择“服务器管理器” 2、在接下来弹出页面中选择“添加角色和功能” 3、接下来点击“下一步” 4、接下来选择“基于角色或基于功能的安装”并点击“下一步” 5、选择“从服务器池中选择服务器”并点击“下一步” 6、接下来选中“Web 服务器(II…

【数模比赛】2023美国大学生数学建模比赛(思路、代码......)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

虚拟存储管理(6)

虚拟存储管理 前面介绍的存储管理方案要求作业全部装入内存才可运行。但这会出现两种情况&#xff1a; 有的作业因太大&#xff0c;内存装不下而无法运行。系统中作业数太多&#xff0c;因系统容量有限只能让少数作业先运行。 1 局部性原理 定义&#xff1a; 程序执行时&a…

TCP网络编程中connect()、listen()和accept()三者之间的关系

基于 TCP 的网络编程开发分为服务器端和客户端两部分&#xff0c;常见的核心步骤和流程如下&#xff1a; connect()函数 对于客户端的 connect() 函数&#xff0c;该函数的功能为客户端主动连接服务器&#xff0c;建立连接是通过三次握手&#xff0c;而这个连接的过程是由内核…

LeetCode题目笔记——24. 两两交换链表中的节点

文章目录题目描述题目链接题目难度——中等方法一&#xff1a;迭代代码/C代码/python方法二&#xff1a;递归代码/C总结题目描述 或许这也是个经典的面试题&#xff0c;记录一手 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在…