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); }); }
在该类中,对 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 ,前面提到数据初始化就是调用了这几个文件中的方法:


// 树形结构的展开和收起
toggleHierarchyState(index: number, bodyStartIndex: number, bodyEndIndex: number) {
//...
}
*/***
** 修改多条数据recordIndexs*
**/*
updateRecords(records: any[], recordIndexs: (number | number[])[]) {
//...
}
//...
布局模块
渲染模块
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 向外暴露了EventManger,EventManager 用来管理表格的各种事件,包括鼠标事件(如点击、双击自动调整列宽、鼠标移动等)和键盘事件(如鼠标滚动、回车键提交编辑内容等)。
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及时回调给用户。