简介
本文将讲解以下几个内容:
-
何为事件模块
-
为什么需要事件模块
-
VTable 中事件的概念
-
VTable 中事件系统的设计与模块划分
何为事件系统
一个项目往往存在多个模块。在开发过程中,一定会出现一个模块依赖于多个模块,多个模块又同时依赖于一个模块的情况,随着项目的增加,单纯依靠各个模块直接交互,会使得项目的耦合度越来越高。

在没有事件系统的时候,如果需要通知影响到的模块,那么每一个触发事件的模块,都需要去通知所有的监听模块,这是一个 n-n 的关系,随着项目越来越大,这种关系的维护会变得十分复杂。

这个时候就轮到事件系统出场了。
事件系统主要的作用是对依赖关系进行解耦,在引入事件系统后,所有的事件管理都可以存放在事件系统中。
事件系统相当于一个中转站,不会去负责业务逻辑、或者说是很少会负责业务逻辑,他只会去监听事件,统一对事件进行下发。其余模块仅需关心事件系统,而不用去费力维护跟其他依赖模块的关系。

从上图可以发现,事件的触发者仅仅会涉及到事件系统,而事件的监听者也只会监听从事件系统触发的事件,这样就能将原来 n-n 的关系转换为 1-n 的关系,降低模块之间的耦合度。
VTable 中的事件
在 JS 中,事件主要是指的浏览器中特定的行为,例如鼠标点击、滚轮滚动,通过事件驱动编程的形式,允许用户来创建交互式的网页。
但是 VTable 中的事件并不局限于浏览器原生事件,内部还包含了自定义事件。

VTable 中同时监听了 自定义事件 和 浏览器原生事件
浏览器原生事件,包括但不限于:
-
touchstart
-
touchcancel
-
touchmove
-
touchend
-
pointermove
-
pointerup
-
pointerdown
自定义事件:不同于浏览器的原生事件,只会在特定的业务逻辑中触发,主要是利用了 VTable 中的发布-订阅模块来实现。自定义事件包括但不限于:
-
CLICK_CELL
-
DBLCLICK_CELL
-
DBLTAP_CELL
-
MOUSEDOWN_CELL
-
MOUSEUP_CELL
-
SELECTED_CELL
-
CONTEXTMENU_CELL
-
CONTEXTMENU_CANVAS
-
DRAG_SELECT_END
事件系统设计
事件系统主要负责几件事,包括 DOM 事件的监听、自定义事件的触发以及更新状态管理模块。我们接下来看下事件系统的模块划分以及实现思路。
VTable 中的事件系统,主要是由下面几个模块来实现的。

EventTarget
- vtable\src\event\EventTarget.ts
EventTarget 作为事件系统的中自定义事件实现的最底层,实现了发布订阅的功能。
VTable 内部有三个重要的模块,都是派生于 EventTarget;

由于 EventTarget 的存在,使得 VTable 能够更方便的创建与监听自定义事件。譬如我们想监听一个图标点击的自定义事件,仅需调用 on 方法,传入对应的回调,那么后续触发事件的时候,会直接执行回调。
// packages\vtable\src\event\event.ts *// 图标点击* this.table.on(TABLE_EVENT_TYPE.ICON_CLICK, iconInfo => { // 改变状态管理模块 });
通过 EventTarget 模块,能够很方便的实现 VTable 事件中的 自定义事件模块。
我们以基本表格的初始化为例,初始化的过程中绑定了包括下面几个主要的自定义事件。

除此之外,VTable 提供的用户自定义注册的功能,也是依托于该模块。

- 这里是 EventTarget 的大致架构
// packages\vtable\src\event\EventTarget.ts
export class EventTarget {
private listenersData: {
listeners,
listenerData
} = {
listeners: {},
listenerData: {}
};
on(type, listener) {
//...
}
off(idOrType, listener): void {
//...
}
addEventListener(type, listener, option): void {
//...
}
removeEventListener(type, listener) {
//...
}
hasListeners(type) {
//...
}
fireListeners(type, event){
//...
}
release(): void {
//...
}
}
EventHandler
EventHandler 主要采用的是观察者和发布订阅模式,他与 EventTarget 的不同点在于:EventTarget 主要负责自定义事件,而EventHandler 主要是负责监听原生 DOM 事件,包括但不限于:
-
copy
-
paste
-
contextmenu
-
resize
-
blur
注册回调的方式跟 EventTarget 方式一样,都是通过 on 方法。不同的点是,EventHandler 主要监听原生的 DOM 元素。
// packages\vtable\src\event\listener\container-dom.ts
handler.on(table.getElement(), 'blur', (e: MouseEvent) => {})
handler.on(table.getElement(), 'keydown', (e: KeyboardEvent) => {})
handler.on(table.getElement(), 'copy', async (e: KeyboardEvent) => {})
handler.on(table.getElement(), 'contextmenu', (e: any) => {})
在 on 方法的实现上面也有所不同,观察源码,我们可以看到,在注册回调事件的时候,会去判断是否存在 addEventListener ,通过该操作即可实现原生 DOM 事件的监听。
// packages\vtable\src\event\EventHandler.ts
export class EventHandler {
on(
target: HTMLElement | Window | EventHandlerTarget,
type: string,
listener: Listener,
...options: any[]
): EventListenerId {
if (Env.mode === 'node') {
return -1;
}
const id = idCount++;
if (target?.addEventListener) {
if (type !== 'resize' || (target as Window) === window) {
(target as EventTarget)?.addEventListener(type, listener, ...(options as []));
} else {
const resizeObserver = new ResizeObserver(target as HTMLElement, listener, this.resizeTime);
this.reseizeListeners[id] = resizeObserver;
}
}
const obj = { target, type, listener, options };
this.listeners[id] = obj;
return id;
}
// ...
}
EventManager
EventManager 是 VTable 的事件管理器,对 VTable 内部的事件做了统一收口,负责大部分事件的监听以及自定义事件的注册,包括原生 DOM 事件以及自定义事件。
- 源码
// packages\vtable\src\event\event.ts
export class EventManager {
constructor(table: BaseTableAPI) {
// 事件绑定,这里包括了场景树中的事件以及原生 DOM 事件
this.bindOuterEvent();
setTimeout(() => {
// 注册自定义事件
this.bindSelfEvent();
}, 0);
}
bindOuterEvent() {
bindTableGroupListener(this);
bindContainerDomListener(this);
bindScrollBarListener(this);
bindTouchListener(this);
bindGesture(this);
}
}
- bindTableGroupListener
我们以 bindTableGroupListener 为例,在函数内部完成了对 tableGroup 提供的外部事件的监听与回调注册。在这些外部事件的回调中,会根据具体的业务逻辑去判断是否要触发自定义事件。
// packages\vtable\src\event\listener\table-group.ts
table.scenegraph.tableGroup.addEventListener('pointermove', (e: FederatedPointerEvent) => {
table.scenegraph.tableGroup.addEventListener('pointerout', (e: FederatedPointerEvent) => {
table.scenegraph.tableGroup.addEventListener('pointerover', (e: FederatedPointerEvent) => {
// ...
- bindSelfEvent
bindSelfEvent 中主要是去注册自定义事件,包括但不限于 ICON_CLICK、DROPDOWN_MENU_CLICK ,而事件注册的功能依赖于 EventTarget 。
// packages\vtable\src\event\event.ts
bindSelfEvent() {
// ...
*// 图标点击*
this.table.on(TABLE_EVENT_TYPE.ICON_CLICK, iconInfo => {
// ...
});
*// 下拉菜单内容点击*
this.table.on(TABLE_EVENT_TYPE.DROPDOWN_MENU_CLICK, () => {
// ...
});
this.updateEventBinder();
*// link/image/video点击*
bindMediaClick(this.table);
*// 双击自动列宽*
this.table.on(TABLE_EVENT_TYPE.DBLCLICK_CELL, (e: MousePointerCellEvent) => {
// ...
});
*// drill icon*
if (this.table.isPivotTable() && checkHaveDrill(this.table as PivotTable)) {
bindDrillEvent(this.table);
}
*// chart hover*
bindSparklineHoverEvent(this.table);
*// axis click*
bindAxisClickEvent(this.table);
*// chart axis event*
bindAxisHoverEvent(this.table);
*// group title checkbox change*
bindGroupTitleCheckboxChange(this.table);
}
简单来说,bindOuterEvent 完成了事件的监听,bindSelfEvent 完成了自定义事件的注册。
结语
VTable 的事件系统,主要是分为两部分:
-
原生 DOM 事件监听,处理 DOM 事件;
-
外部事件监听,包括 Stage 冒泡上来的事件,根据具体条件判断是否需要触发自定义事件。
通过将表格的交互拆成 事件模块 和 状态模块,事件模块主要触发监听和触发,状态模块负责表格内部状态的维护,实现 事件变化 -> 状态变更 -> 表格渲染 的逻辑。这种模块方式能够更好的降低项目模块之间的耦合度。
本文档由以下人员提供
taiiiyang(https://github.com/taiiiyang)