桥接模式:跨平台的事件机制设计

内容列表

对于 Web 的图表组件库来说,一些功能比较强大的开源库,渲染层可以支持 DOM、SVG、Canvas、WebGL 等多个平台的环境,而图表库的很多功能的实现都和渲染层紧密相关。

最近,在参考学习一些开源的图表组件库时,发现在跨平台设计中,事件机制的实现很有意思,所以在这里以最简化的代码来解释和记录一下这个方案。如果要用经典的软件设计模式来解释,大概就是桥接模式了。

桥接模式(Bridge Pattern) 将一个功能的实现拆分为抽象(Abstraction)和实现(Implementor),让其相互独立的扩展和定义,借助该模式可以设计一种平台无关的软件架构。

事件机制

事件机制是软件设计中最基础、最为常见的一种设计,对于 Web 图表组件库来说要提供一些处理用户交互(例如点击、拖动、右键点击等)的机制。一个典型的事件模型类如下:

class EventEmitter {
  _handlerMap = {};
  on(event, callback) {}
  off(event, callback) {}
  emit(event, ...args) {}
}

对于用户来说,对外暴露 on()off() 方法来注册和取消事件,而图表库内部需要完成事件触发(emit())的实现,而这里与渲染层耦合。以渲染层为 DOM 实现来举例,支持点击事件:

class Chart {
  constructor() {
    // 渲染层为 DOM 实现
    this.__renderer = new DOMRenderer();
    this._handler = new EventEmitter();
  }

  on(...args) {
    this._handler.on(...args);
  }

  off(...args) {
    this._handler.off(...args);
  }

  __bindEvent() {
    // ! 事件触发(绑定)与渲染层耦合
    this.__renderer.domElem.addEventListener('click', (event) => {
      this._handler.emit('click', ...[event, ...otherArgs]);
    });
  }
}
跨平台实现

参考桥接模式,这里可以把图表类中的事件机制实现拆分为抽象(Handler)和实现(HandlerProxy),前者管理用户注册的事件池,后者负责特定平台的事件触发实现。示例代码如下:

class Handler extends EventEmitter {
  constructor(handlerProxy) {
    super();

    this.__handlerProxy = handlerProxy;

    // 注册事件到代理类中
    this.__handlerProxy.on('click', (event, ...args) => {
      // ! 触发用户注册的事件
      this.emit(event, ...args);
    });
  }
}

class DOMHandlerProxy extends EventEmitter {
  constructor(renderer) {
    super();

    this.__renderer = renderer;
  }

  __bindEvent() {
    // 根据渲染层的平台实现事件绑定,以 DOM 实现为例
    this.renderer.domElem.on('click', (event, ...args) => {
      // ! 触发 Handler 注册的事件
      this.emit(event, ...args);
    });
  }
}

对于图表类来说,Handler 类提供了完整的事件机制,但其内部把具体平台相关的事件触发实现交给 HandlerProxy 类去实现。这样就完成了事件机制的实现与特定平台实现分离的目标,针对不同平台实现不同的HandlerProxy 类即可。现在图表类的代码应该如下:

class Chart {
  constructor() {
    // 渲染层为 DOM 实现
    this.__renderer = new DOMRenderer();
    this._handler = new Handler(new DOMHandlerProxy(this.__renderer));
  }

  on(...args) {
    this._handler.on(...args);
  }

  off(...args) {
    this._handler.off(...args);
  }
}

现在来看,图表类中之前事件触发实现与平台相关的代码已经被独立出去,且可以根据不同的渲染层实现完成无缝衔接。

结语

以上就是利用桥接模式对跨平台的事件机制的简化设计,解决此类问题时,最重要的是划分抽象实现两部分。

参考

相关

DOM-文本节点

文本(Text)节点虽然很多时候我们直接用 innerHTML 去赋值替换,但当我们进行一些细微的修改时,了解一下 DOM 操作还是非常有用的。

了解更多

微处理器寻址范围

在此之前,让我们带着下面这个问题来看这篇文章:64 位处理器所支持的最大内存(寻址范围)为多少?

了解更多

CSS 清除浮动

在浮动布局中,有时候会因为父元素没有设置高度而子元素浮动导致父元素坍塌,我们就需要清除浮动撑起父元素的高度,在这里总结一下常用方法。

了解更多