!!!###!!!title=4.1 VTable 事件设计——VisActor/VTable 社区贡献者文档!!!###!!!!!!###!!!description=--- title: 4.1 VTable 事件设计 key words: VisActor,VChart,VTable,VStrory,VMind,VGrammar,VRender,Visualization,Chart,Data,Table,Graph,Gis,LLM --- !!!###!!!

简介

本文将讲解以下几个内容:

  • 何为事件模块

  • 为什么需要事件模块

  • 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)

本文档由以下人员修正整理

玄魂