!!!###!!!title=1.2 VTable 的基本架构和源码结构——VisActor/VTable 社区贡献者文档!!!###!!!!!!###!!!description=---title: 1.2 VTable 的基本架构和源码结构 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM--- !!!###!!!

VTable 核心架构

结构分析

VTable 可以大致分成几个模块:数据处理层、布局模块、渲染模块、事件模块、交互模块、状态管理、组件通信。

  • 数据处理层负责对原始的数据进行管理,处理类似行列转置、内置的排序的操作,并将处理后的数据重新交给表格渲染;

  • 布局模块主要负责基本表头布局、透视表表头布局、单元格布局、行列高度的计算算法,并将计算后的布局交给渲染层进行渲染;

  • 渲染模块主要调用 vrender 进行图表及各图元的渲染并且依赖于布局模块;

  • 交互模块处理各种用户的点击操作,类似点击表头排序,编辑单元格等;

  • 事件模块处理内部的自定义事件,包括生命周期事件、列宽与行高调整、表格滚动等;

  • 状态模块处理 hover 、select、menu、sort 等状态的存储;

各个不同模块之间通过EventTarget实现了发布订阅。

数据处理层

该层主要是接收传入的数据并存储数据、同时对数据进行基本的转换,包括聚合和排序等操作。

ListTable 的初始化过程中,对于传入的数据,会进行一些基本操作,首先将 options.records 或者 options.dataSource 传递给CachedDataSource 进行实例化,再将实例放入table.internalProps.dataSource提供给组件内部使用,比如说进行表格进行布局计算的时候。

数据层的初始化:

  • 第一步 ListTable 初始化的时候去处理 dataSource 或 records

  • 第二步 通过不同的判断将 dataSource 或 records 放入 internalProps 中,后续对数据的变更便可直接操作 dataSource 对象

  • VTable\packages\vtable\src\ListTable.ts
constructor(container?: HTMLElement | ListTableConstructorOptions, options?: ListTableConstructorOptions) {
    //...
    if (options.dataSource) {
      _setDataSource(this, options.dataSource);
    } else if (options.records) {
      this.setRecords(options.records as any, { sortState: internalProps.sortState });
    } else {
      this.setRecords([]);
    }
    //...
}

  • _setDataSource_setRecords 方法:VTable\packages\vtable\src\core\tableHelper.ts
// addReleaseObj 方法可以粗略概括为调用回调函数去更新组件内部的数据
export function _setDataSource(table: BaseTableAPI, dataSource: DataSource): void {
  _dealWithUpdateDataSource(table, () => {
    if (dataSource) {
      if (dataSource instanceof DataSource) {
        table.internalProps.dataSource = dataSource;
      } else {
        // 如果是初次调用该方法,会将 dataSource 初始化为 CachedDataSource 的实例,后续如果更新就不会重复 new
        const newDataSource = (table.internalProps.dataSource = new CachedDataSource(dataSource));
        table.addReleaseObj(newDataSource);
      }
    } else {
      table.internalProps.dataSource = DataSource.EMPTY;
    }
    table.internalProps.records = null;
  });
}

export function _setRecords(table: ListTableAPI, records: any[] = []): void {
  _dealWithUpdateDataSource(table, () => {
    table.internalProps.records = records;
    // 这里通过调用 CachedDataSource.ofArray 方法将records转换为实例所需的dataSource结构
    const newDataSource = (table.internalProps.dataSource = CachedDataSource.ofArray(
      records,
      table.internalProps.dataConfig,
      table.pagination,
      table.internalProps.columns,
      table.internalProps.layoutMap.rowHierarchyType,
      getHierarchyExpandLevel(table)
    ));
    // 可以看到这里调用 addReleaseObj 方法将 CachedDataSource 处理过的records 存放进 table.internalProps.dataSource 中
    table.addReleaseObj(newDataSource);
  });
}

CachedDataSource.ofArray 方法内部实际上也是 new CachedDataSource,用于对 records 进行适配。
* VTable/packages/src/data/CachedDataSource.ts

在该类中,对 DataSource 实现了缓存的操作。同时继承了 DataSource

export class CachedDataSource extends DataSource {
  // ...
  static ofArray(
    array: any[],
    dataConfig?: IListTableDataConfig,
    pagination?: IPagination,
    columns?: ColumnsDefine,
    rowHierarchyType?: 'grid' | 'tree',
    hierarchyExpandLevel?: number
  ): CachedDataSource {
    return new CachedDataSource(
      {
        get: (index: number): any => {
          return array[index];
        },
        length: array.length,
        records: array
      },
      dataConfig,
      pagination,
      columns,
      rowHierarchyType,
      hierarchyExpandLevel
    );
  }
  //...
}

  • VTable/src/data/DataSource.ts

来到 DataSource 类中,可以看到,在 DataSource 中实现了对records的托管操作,对原始数据的处理都会抽离到该模块中。

 export interface DataSourceParam {
  get?: (index: number) => any; // 这里是对数据的代理
  length?: number;
  */** 需要异步加载的情况 请不要设置records 请提供get接口 */*
  records?: any;
  // records 的增删操作
  added?: (index: number, count: number) => any;
  deleted?: (index: number[]) => any;
  canChangeOrder?: (sourceIndex: number, targetIndex: number) => boolean;
  changeOrder?: (sourceIndex: number, targetIndex: number) => void;
}

export class DataSource extends EventTarget implements DataSourceAPI {
 constructor(
    dataSourceObj?: DataSourceParam,
    dataConfig?: IListTableDataConfig,
    pagination?: IPagination,
    columns?: ColumnsDefine,
    rowHierarchyType?: 'grid' | 'tree',
    hierarchyExpandLevel?: number
  ) {
    super();
    // ...
    this.dataSourceObj = dataSourceObj;
    this.dataConfig = dataConfig;
    this._get = dataSourceObj?.get;
    this.columns = columns;
    this._source = dataSourceObj?.records ? this.processRecords(dataSourceObj?.records) : dataSourceObj;
    }
}

逻辑层主要是涉及各类逻辑的处理,类似图表转置、数据排序、筛选逻辑与合计。主要的数据处理逻辑都是存放在这几个文件当中,包括暴露给外部调用的 API ,前面提到数据初始化就是调用了这几个文件中的方法:

VTable\packages\vtable\src\data\CachedDataSource.ts VTable\packages\vtable\src\data\DataSource.ts VTable\packages\vtable\src\dataset\dataset.ts VTable\packages\vtable\src\dataset\dataset-pivot-table.ts
比如以下两个 API 的核心逻辑:
* VTable\packages\vtable\src\data\DataSource.ts
// 树形结构的展开和收起
toggleHierarchyState(index: number, bodyStartIndex: number, bodyEndIndex: number) {
    //... 
 }
 
*/***
** 修改多条数据recordIndexs*
**/*
updateRecords(records: any[], recordIndexs: (number | number[])[]) {
  //...
}
//...

布局模块

布局模块的核心文件位于 VTable\packages\vtable\src\layout 中
布局模块 Layout 是 VTable 组件核心的模块,包括了基本表格的表头、透视表表格的表头、图表单元格的布局,树形表头和树形单元格的逻辑,通过布局模块完成调整后,再去交给渲染层进行渲染。除此之外,该文件内部还包含了很多关于布局调整的辅助逻辑。

渲染模块

VTable 的核心渲染能力是通过 VRender 实现的,其内部借用了 VRender的能力去实现表格的初始化渲染、数据更新与删除后的重新渲染、用户拖拽后的重新渲染等等。

在 VTable 内部维护了一份场景树 (scenegraph),场景树中定义了 VTable 中的核心渲染逻辑。

场景树的初始化和render方法的定义位于 BaseTable中:

  • VTable/packages/vtable/src/core/BaseTable.ts
export abstract class BaseTable extends EventTarget implements BaseTableAPI {
  //....
  constructor(container: HTMLElement, options: BaseTableConstructorOptions = {}) {
  //...
      this.scenegraph = new Scenegraph(this);
   //...
  }
  
  */***
*   * 重绘表格(同步绘制)*
*   */*
  render(): void {
    this.scenegraph.renderSceneGraph();
  }
 }

  • VTable/packages/vtable/src/scenegrpah/scenegrapg.ts
*/***
* * @description: 表格场景树,存储和管理表格全部的场景图元*
* * @return {*}*
* */*
export class Scenegraph {
  //...
  stage: IStage;
  //...
   
  */***
*   * @description: 绘制场景树*
*   * @param {any} element*
*   * @param {CellRange} visibleCoord*
*   * @return {*}*
*   */*
  renderSceneGraph() {
    this.stage.render();
  }

初次渲染的入口是 BaseTable.render 方法,内部调用 scenegraph 中的 renderSceneGraph,转而通过 VRender 中 createStage 创建的 Stage 上的 render 方法去将表格绘制到指定的 DOM 节点,至此第一步的渲染就完成了,后续的渲染层逻辑都是通过 scenegraph 去调用的。

交互模块

交互模块主要处理两样工作:

  • 监听表格自定义事件

  • 更改表格相关状态、重新渲染表格

交互模块主要是由事件模块和状态管理构成,完成 hover高亮、select 高亮、表格滚动等操作。

事件模块

  • VTable\packages\vtable\src\event\event.ts

event.ts 向外暴露了EventMangerEventManager 用来管理表格的各种事件,包括鼠标事件(如点击、双击自动调整列宽、鼠标移动等)和键盘事件(如鼠标滚动、回车键提交编辑内容等)。

VTable 在内部通过 VRender 提供的 Stage.addEventListener 来监听大部分表格内部自定义事件,包括 wheel、click 事件。比如 wheel 事件用来实现表格滚动;click 用来改变 selectState 同时触发外部传入 click_cell 回调。

状态管理

全局的状态管理:这里的状态是独立于表格存在的,在状态进行变更时会去重新绘制表格,StateManager 主要包括以下几个部分:

  • hoverState 表格 hover 的配置和当前 hover 的单元格

  • selectState 表格当前选中的单元格

  • frozen 冻结的行或列

  • scroll 当前表格滚动到的水平与垂直位置

  • sparkLine 迷你图的高亮状态

  • sort 内部的自定义排序

这些数据都是定义在 StateManager 中,并在表格初始化的时候生成。通过更新 State,能够触发重新绘制表格。StateManager 核心文件定义在:VTable\packages\vtable\src\state\state.ts

组件通信

VTable 的组件通信部分是依赖于 EventTarget类,通过观察源码结构,会发现大部分模块都会继承 EventTarget,通过EventTarget 实现事件通信的功能。

  • vtable/src/data/Datasource.ts
export class DataSource extends EventTarget implements DataSourceAPI { //...

  • vtable/src/core/BaseTable.ts
export abstract class BaseTable extends EventTarget implements BaseTableAPI { //...

  • vtable/src/header-helper/style/Style.ts
export class Style extends EventTarget implements ColumnStyle {

EventTarget内部结构:

  • vtable/src/event/EventTarget.ts
import type {
  TableEventListener,
  EventListenerId,
  TableEventHandlersEventArgumentMap,
  TableEventHandlersReturnMap
} from '../ts-types';
import { isValid } from '@visactor/vutils';

let idCount = 1;

export class EventTarget {
  private listenersData: {
    listeners: { [TYPE in keyof TableEventHandlersEventArgumentMap]?: TableEventListener<TYPE>[] };
    listenerData: {
      [id: number]: {
        type: string;
        listener: TableEventListener<keyof TableEventHandlersEventArgumentMap>;
        remove: () => void;
      };
    };
  } = {
    listeners: {},
    listenerData: {}
  };

  */***
*   * 监听事件*
*   * @param type 事件类型*
*   * @param listener 事件监听器*
*   * @returns 事件监听器id*
*   */*
  on<TYPE extends keyof TableEventHandlersEventArgumentMap>(                        
    type: TYPE,
    listener: TableEventListener<TYPE>
  ): EventListenerId {
    ...
  }

  off(type: string, listener: TableEventListener<keyof TableEventHandlersEventArgumentMap>): void;
  off(id: EventListenerId): void;
  off(
    idOrType: EventListenerId | string,
    listener?: TableEventListener<keyof TableEventHandlersEventArgumentMap>
  ): void {
     // ...
  }
    
   // 添加事件监听
  addEventListener<TYPE extends keyof TableEventHandlersEventArgumentMap>(
    type: TYPE,
    listener: TableEventListener<TYPE>,
    option?: any
  ): void {
    this.on(type, listener);
  }
    
  // 移除事件监听
  removeEventListener(type: string, listener: TableEventListener<keyof TableEventHandlersEventArgumentMap>): void {
      // ...
  }

  hasListeners(type: string): boolean {
      // ...
  }
  
  // 触发事件
  fireListeners<TYPE extends keyof TableEventHandlersEventArgumentMap>(
    type: TYPE,
    event: TableEventHandlersEventArgumentMap[TYPE]
  ): TableEventHandlersReturnMap[TYPE][] {
      // ...
  }
  
  // 释放事件监听
  release(): void {
    delete this.listenersData;
  }
}


  • 内部组件通信:

  • 不同层次之间通过事件、函数调用或数据共享等方式进行通信。例如,当交互模块收到用户的排序操作请求时,会调用逻辑处理层的排序函数,并将排序结果传递给渲染层进行重新渲染。

  • 渲染层可能会根据数据层的数据更新,触发交互模块的某些操作,如更新选中状态的显示等。

  • 外部回调:

  • 外部传入进来的事件订阅,通过 fireListeners 进行回调,比如不同的生命周期事件,都会通过 fireListenrs及时回调给用户。

整体模块设计

VTable 源码结构

VTable 模块概览

  • 数据处理层

Options 用户传入的表格配置,用来描述表格的结构、内容和样式;

DataSource 负责管理数据,定义了数据的读取与操作数据的方法;

Dataset 透视表的数据解析模块;

  • 布局模块

Layout 负责基本表格和透视表表头的布局计算、单元格行列高度计算等;

  • 渲染模块

Scenegraph 模块负责表格场景节点的创建与更新;

SceneProxy 是 Scenegraph 的子模块,负责初始化最大展示行列数的计算、场景树的初始化逻辑、表格渐进式渲染的逻辑;

Theme 模块管理表格的全局样式,给单元格提供样式;

  • 状态管理

StateManager 状态管理负责管理表格当前的状态,包括冻结、选中、hover、滚动等等的表格状态;

  • 事件模块

EventManager 负责管理自定义事件的定义和监听;

  • 组件通信

EventTarget 提供了发布订阅模式、负责不同模块之间进行事件通信的操作;

Vtable 目录结构

通过观察 src 文件夹下的目录,可以看到大致的项目结构,可以看到都是严格按照模块进行划分。基本上每个文件都会向外导出一个 Class,通过在不同时机不同位置构建实例,完成 VTable 的初始化。

下图大致展示了 VTable 的代码结构和各个文件所负责的功能

路径分析

src/index.ts 出发,可以看到 VTable 从此处向我们暴露了最常用的两个组件,ListTable 和 PivotTable ,这里就是 VTable 不同组件进行收口的地方。

  • vtable/packages/vtable/src/index.ts
import { graphicUtil, registerForVrender } from '@src/vrender';
registerForVrender();
// ...
import { ListTableAll as ListTable } from './ListTable-all';
import { PivotTableAll as PivotTable } from './PivotTable-all';
//...
export {
  //...,
  ListTable,
  //...
  PivotTable,
  PivotChart,
};
//...

进入到ListTable-all,可以看到,在ListTable初始化的时候注册了需要用到的组件,以便图表实例能够在不同的地方调用。

  • VTable\packages\vtable\src\ListTable-all.ts
import { ListTable } from './ListTable';
// ...

registerAxis(); // 注册坐标轴
registerEmptyTip(); // 注册 EmptyTip 组件
// ...
registerVideoCell();
export class ListTableAll extends ListTable {}

  • VTable/packages/vtable/src/ListTable.ts
export class ListTable extends BaseTable implements ListTableAPI {

在 BaseTable 中,定义了最核心的几个模块。

Scenegraph 场景树 StateManager 状态管理 EventManager 交互事件管理
* VTable/packages/vtable/src/core/BaseTable.ts
export abstract class BaseTable extends EventTarget implements BaseTableAPI {
  constructor {
      ...
      this.scenegraph = new Scenegraph(this);
      this.stateManager = new StateManager(this);
      this.eventManager = new EventManager(this);   
      this.animationManager = new TableAnimationManager(this);
      ...
  }     
}

BaseTable 继承了 EventTarget 类,用于实现发布订阅的操作。

  • 对 VTable 进行路径分析之后,我们能够画出 ListTable 各模块大致的引用图:

PivotTable 也是采用类似的架构,与`ListTable`相同,都是继承了 BaseTable。
### 结语

VTable 通过对各个不同功能进行合理的模块化管理,能够最大化的提升开发效率和降低上手成本。

本文通过对 VTable 进行分层的方法,将 VTable 的结构进行大致的划分,抽成数据层、逻辑层、渲染层和交互模块,通过对不同层次进行解析,介绍了 VTable 的基本架构。

随后又从目录入手,介绍了 VTable 的源码结构、各个模块划分及不同模块负责的功能。在从 2 个角度进行分析之后,能够对 VTable 整体的架构有一定的了解。

本文档由以下人员提供

taiiiyang( https://github.com/taiiiyang)