• Antv/s2 明细表 透视表实现和性能优化(一)


    前言

            以我实际项目环境为准,vue+ts为技术框架,代码如果有什么不懂欢迎留言评论我会回复的

    透视表

     定义文件

    1. class PivotTableControl extends BaseControl {
    2. type = 'pivotTable';
    3. label = 'controls.chart.pivotTable';
    4. icon = 'tc-color-pivot-table';
    5. widget = () => ({
    6. ...super.widget(),
    7. // 数据源表单
    8. relationForm: null,
    9. // 数据范围
    10. range: 'all',
    11. // 行维度
    12. xDimension: [],
    13. // 列维度
    14. yDimension: [],
    15. // 指标
    16. metrics: [],
    17. // 过滤
    18. filter: null,
    19. // 图表配置
    20. typeConfig: chartDefaultConfigs()[this.type],
    21. // 背景
    22. background: null,
    23. });
    24. }

            不用关心BaseControl有什么,看看几个特有的属性:

            xDimension: 行维度

            yDimension: 列维度

            metrics:指标

    维度和指标

            业务设计的概念,方便我们理解和开发。先看例子

            一个行维度,一个指标

            

            一个行维度,两个指标

            

            两个行维度,一个指标

            

            到这里,可以看到,行维度就是分组依据,而指标就是对应分组依据的值。

            两个行维度,一个指标

            

    在透视表里的维度和指标

             维度(行):分类依据,在「透视表」中指代的是透视表的行数将会以该组件数据进行划分。(如:行维度使用了字段「成员名称」,成员中包含5个不同的字符,于是透视表将会拆分成5行,每一行的命名标题将以「成员名称」的各字符命名。)

            维度(列):跟行维度一样,也是分类依据,在「透视表」中指代的是透视表的列数将会以该组件数据进行划分。(如:列维度使用字段「性别」其中学科包含男女,于是透视表将会划分成2列,分别对应男、女。)

            指标:指标是对维度的量化。在透视表中,他是指代除「列表头」、「行表头」以外的单元格展示的数值,而该数值是从属于相应行和列维度的数据汇总结果(如某单元格的表示的是行为某个成员,列是语文科目的成绩,代表该成员的语文成绩。)

    实例:这里是一行维度一指标

     两行维度一指标

    一行维度一列维度一指标

    两行维度一列维度一指标

    搞个复杂点的!两行维度两列维度两指标

    S2

            项目里用的功能有这三个

    import { PivotSheet, S2Event} from '@antv/s2';

            PivotSheet是它的渲染表格组件,S2Event是事件列表

            S2的设计是把我们行列维度的划分放在了角头中,行头列头都是划分的数据。

    S2-封装

          我们要先封装一个配置好的S2表格组件。

    1. const s2OptionsDefault = {
    2. width: 0,
    3. height: 0,
    4. interaction: {
    5. resize: {
    6. cornerCellHorizontal: true, // 角头水平
    7. rowCellVertical: true, // 行头垂直
    8. colCellHorizontal: true, // 列头水平
    9. colCellVertical: true, // 列头垂直
    10. },
    11. hoverHighlight: false, // 不开启hover聚光灯效果, 提高性能
    12. hoverFocus: false,
    13. brushSelection: false,
    14. multiSelection: false,
    15. rangeSelection: false,
    16. enableCopy: true,
    17. },
    18. pagination: {
    19. pageSize: 100,
    20. },
    21. frozenRowHeader: true,
    22. style: {
    23. cellCfg: {
    24. height: 34,
    25. },
    26. colCfg: {
    27. height: 34,
    28. },
    29. },
    30. hierarchyType: 'grid',
    31. conditions: {},
    32. };
    33. const s2DataConfigDefault = {
    34. fields: {
    35. rows: [],
    36. columns: [],
    37. values: [],
    38. valueInCols: true,
    39. },
    40. meta: [],
    41. data: [],
    42. };

            先写好自己想要的默认配置。创建一个代理类VirtualTableProxy 

    1. export class VirtualTableProxy {
    2. #instance;
    3. #options;
    4. #dataConfig;
    5. // 实例
    6. get instance() {
    7. return this.#instance;
    8. }
    9. // 配置
    10. get options() {
    11. return this.#options;
    12. }
    13. // 数据
    14. get dataConfig() {
    15. return this.#dataConfig;
    16. }
    17. setOptions(path, value) {
    18. if (!this.#options || typeof path === 'object') {
    19. this.#options = path;
    20. } else {
    21. set(this.#options, path, value);
    22. }
    23. }
    24. setDataConfig(path, value) {
    25. if (!this.#dataConfig || typeof path === 'object') {
    26. this.#dataConfig = path;
    27. } else {
    28. set(this.#dataConfig, path, value);
    29. }
    30. }
    31. setInstance(instance) {
    32. this.#instance = instance;
    33. }
    34. }

            这个类有什么用,我们后面讲

    1. // s2-component
    2. created() {
    3. // 初始化实例
    4. this.VT = new VirtualTableProxy();
    5. this.VT.setOptions(deepClone(s2OptionsDefault));
    6. this.VT.setDataConfig(deepClone(s2DataConfigDefault));
    7. }

           render函数:

    1. render() {
    2. return (
    3. <div>
    4. <div id={this.componentId} class={this.$style.container}>div>
    5. div>
    6. );
    7. }
    8. componentId = `container_${this._uid}`;

            这里的uid你可以理解为组件的hash标识

            等待模板渲染后:

    1. mounted() {
    2. const container = document.getElementById(this.componentId);
    3. this.container = container;
    4. this.VT.setInstance(
    5. new PivotSheet(container, this.VT.options, this.VT.dataConfig),
    6. );
    7. }

            这里的setIntance只是把这个s2生成的表格实例存放到VirtualTableProxy 类里的私有属性而已

            注册S2事件监听

    1. mounted() {
    2. ....
    3. this.VT.instance.on(S2Event.DATA_CELL_CLICK, this.dataCellClick);
    4. this.VT.instance.on(S2Event.LAYOUT_AFTER_RENDER, () => {
    5. this.$emit('finishRender');
    6. });
    7. }

                其实几乎就是把事件抛出去给外部处理,毕竟s2-component只是一个加载组件而已

                到这里,这个组件已经基本完成。我们做的只是创建一个代理类用来存储配置、表格实例,同时在组件上渲染了一个初始化的表格。那还差什么?数据注入。

            从外部调用S2-component

            index.vue调用s2-component

    1. // 透视表组件主体文件 index.vue

            

    1. created() {
    2. this.init();
    3. }
    4. init() {
    5. ...
    6. // 转换数据结构
    7. const tableData = this.generateCommonData({
    8. cornerHeader: columnItem,
    9. colHeader: columnDimension,
    10. rowDimensionCategory: rowItem,
    11. metricsCategory: indicatorItem,
    12. rowHeader: rowDimension,
    13. dataCell: data,
    14. summaryResult: {
    15. rowSummary: result.summaryResult?.rowSummary || [],
    16. colSummary: result.summaryResult?.colSummary || [],
    17. },
    18. });
    19. ...
    20. }

            前面都是业务逻辑,将接口端的数据转换成S2表格支持的数据格式。like be...

                    这个数据结构呢,类似于上文提到的s2DataConfigDefault。不出意外的话,tableData 将会覆盖原来的s2DataConfigDefault。

    1. init() {
    2. // 避免Vue依赖收集
    3. this.data = new PivotTableProxy(tableData);
    4. }
    5. export class PivotTableProxy {
    6. #data;
    7. constructor(data) {
    8. this.#data = data;
    9. }
    10. get data() {
    11. return this.#data;
    12. }
    13. }

            往下看,你会发现this.data被赋值了给一个类,而tableDate作为类的私有属性被储存起来。这里要引申一个概念,Vue的依赖收集。

    Vue的依赖收集和性能问题-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/weixin_42274805/article/details/128685452?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169674882416800180624606%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=169674882416800180624606&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-128685452-null-null.nonecase&utm_term=%E4%BE%9D%E8%B5%96%E6%94%B6%E9%9B%86&spm=1018.2226.3001.4450        不懂可以看我这篇,这里为什么要绕开Vue的依赖收集呢。因为依赖收集需要对数据元进行递归绑定。当数据元数据量过大(例如明细表或透视表数十万条数据),会严重影响前端的性能效率。这么去藏值会让效率提升很多哦,特别是几十万条数据的情况下。但是,透视表的渲染将由前端自行控制,而不是由Vue框架的响应式决定。

    1. init() {
    2. ...// 接上文
    3. // 手动渲染透视表
    4. this.$nextTick(() => {
    5. this.$refs.virtualTableS2?.setData(this.data.data);
    6. });
    7. }

            看到这里,就该回去S2-component里补完数据注入逻辑啦

    S2-封装-数据注入

            上面提到的setData方法。我们来看看怎么实现

    1. setData(pivotTableData) {
    2. const {
    3. fields: { rows, columns, values },
    4. meta,
    5. data,
    6. } = pivotTableData;
    7. // 配置
    8. this.pagination.total = this.total;
    9. this.VT.setOptions('frozenRowHeader', this.fixedHeaderY);
    10. this.VT.setOptions('style.cellCfg.height', this.field.widget.lineHeight);
    11. this.VT.setOptions('style.colCfg.height', this.field.widget.lineHeight);
    12. this.VT.setOptions('hierarchyType', this.field.widget.hierarchyType);
    13. // 数据
    14. this.VT.setDataConfig('fields.rows', rows);
    15. this.VT.setDataConfig('fields.columns', columns);
    16. this.VT.setDataConfig('fields.values', values);
    17. this.VT.setDataConfig('meta', meta);
    18. this.VT.setDataConfig('data', data);
    19. this.reRender();
    20. }

            操作很简单,根据数据去更改VT实例里的属性配置

    1. // 重新渲染S2
    2. reRender() {
    3. this.VT.instance.setOptions(this.VT.options);
    4. this.VT.instance.setDataCfg(this.VT.dataConfig);
    5. this.VT.instance.render(true);
    6. }

            上调用VT实例里的S2表格组件实例,填充配置和数据配置,并执行渲染。

            看到这里,大家应该知道为什么要设置VirtualTableProxy类了吧,一方面是为了绕开数据绑定,第二方面是为了把数据、配置、视图模块化。

    性能对比

            上面提到过,绕过Vue的依赖收集会有巨大的性能提升。现在来看看效果:

            为了快速定位方法,我把上面的init改成testIn。打开浏览器的性能标签页,开始性能检测。

            这是绕过的:

     this.data = new PivotTableProxy(tableData);

           看到testin方法一共耗时1.03秒,其中this.generateCommonData占用了1.03秒,这个方法就是我们前端遍历转换数据的。

            这是没有绕过的:

    this.data = tableData

             此时testin方法耗时了1.91秒,其中generateCommonData耗时仅1.08秒,与上面接近,那么多出来的800多毫秒,是黄色部分组成的。

            看到observer和defineReactive了吗?想到什么?Vue2的双向绑定啊!!Vue对data数据递归后逐个绑定观察者,上面的实验仅有124条数据,便要产生800毫秒的延迟!如果是124万条数据呢?不用怀疑,直接卡死!所以我们才要绕过Vue的依赖收集

  • 相关阅读:
    Mybatis中如何在mapper.xml中为date类型作为where条件呢?
    MVC医院信息管理系统源码 BS架构
    python语义分割标签的游程长度编码和解码
    【AWVS破解安装学习】
    bacnet cov机制详细介绍
    全球首款 RISC-V 笔记本 ROMA 正式发布!
    Spring Boot 整合 分布式搜索引擎 Elastic Search 实现 数据聚合
    ES学习看这一篇文章就够了
    2022年第四届计算机视觉与模式识别国际会议(CCVPR 2022)
    LayUI之CRUD
  • 原文地址:https://blog.csdn.net/weixin_42274805/article/details/133671240