最近因为在开发一款设计器,需要自己实现一套事件系统,项目基于TypeScript实现,其包含事件的添加、删除、触发等功能。 目前主要采用了观察者模式,以下是基本的实现思路:
基于Map的事件调度存储
我使用Map来存储事件名称和回调函数数组的映射,以便快速查找指定事件名称对应的回调函数。在添加事件监听时,会先检查该事件是否已经存在,如果存在则直接将回调推入数组,如果不存在则先创建数组再推入。删除事件监听时,需要先在数组中找到对应的回调索引,然后进行splice删除。
触发方式
对于事件的触发,提供了三种触发方式:
-
emitEvent,这是一种异步触发方式,会通过Promise包裹,确保所有的回调执行完成。
-
emitEventSync,这是一种同步触发方式,会直接循环调用回调函数。
-
emitEventIterable,这是一种利用迭代器实现的可阻断同步触发方式。通过yield关键字可以返回结果控制后续回调是否继续。
使用上,首先需要通过addEventListener添加事件监听,然后通过emitEvent、emitEventSync或emitEventIterable触发事件并传入参数。当不需要某事件时,可以通过removeEventListener移除监听。
调用示例
自定义事件
当一个页面内容发生变化时候,我们需要定义一个事件监听机制,并在执行updatePageData逻辑后通知到每一个订阅事件的模块。
这里要注意的是代码中的this.app.facade.event=new Event(); ,这是我设计的系统在引导启动过程中注册到facade上的一个provider ,有兴趣的同学可以了解一下服务提供者以及门脸模式,而在这个事件系统的实现里,我们主要参考了观察者模式,他们所解决的问题不同:
- Facade模式致力于简化复杂系统的外部 API 设计。
- Provider模式旨在实现服务的解耦与替换。
- Observer关注的是对象间的观察-通知关系,它通过订阅-发布机制实现了对象间的联动。
class Page extends BaseApi {
//定义一个基础的事件常量,用于标记事件类型
public static readonly EVENT_ON_CHANGE = 'pageOnChangeEvent';
//实现自定义订阅方法
public onChange(handler: (params: PageInfo[]) => void) {
this.app.facade.event.addEventListener(Page.EVENT_ON_CHANGE, handler);
}
//更新页面数据后并发送事件通知
public updatePageData() {
//todo:修复并保存
//通知
this.app.facade.event.emitEvent(Page.EVENT_ON_CHANGE, this.pages);
}
}
可阻断的事件
通过yield和return false可以实现一些比较复杂的控制流,这在一些需要顺序执行或条件触发的场景下很有用,比如说在页面跳转的时候进行拦截权限的动作,当没有权限的时候,我们ruturn false;即可触发中断后续事件通知。
//这是一个利用迭代器实现的可阻断的同步的事件触发器
emitEventIterable(eventName: string, ...args: any[]): boolean {
if (this._events.has(eventName)) {
const callbacks = this._events.get(eventName) || [];
const eventIterable = function* () {
for (let i = 0; i < callbacks.length; i++) {
yield callbacks[i](...args);
}
}
const events = eventIterable()
while (true) {
//通过迭代器的方式保证执行顺序,并保证可阻断事件继续向后传播
const result = events.next();
if (result.value === false) {
return false;
}
if (result.done) {
return true;
}
}
}
return true;
}
完整代码
接口定义
我定义了一个IEvent接口,它包含事件系统需要实现的基本方法,如获取所有事件、获取某个事件的回调函数数组、添加和删除事件监听器等。在EventListener类中实现了该接口。
interface IEvent {
_events: Map<string, Function[]>;
getEvents(): Map<string, Function[]>;
getEvent(eventName: string): Function[];
removeEvent(eventName: string): void;
addEventListener(eventName: string, callback: Function): void;
removeEventListener(eventName: string, callback: Function): void;
//这是一个异步的事件触发器
emitEvent(eventName: string, ...args: any[]): void;
//这是一个同步的事件触发器
emitEventSync(eventName: string, ...args: any[]): void;
//这是一个可阻断的同步的事件迭代触发器
emitEventIterable(eventName: string, ...args: any[]): boolean;
}
export default IEvent;
完整实现
这个类,您可以理解为事件系统的中央总线,它可以在不同的组件之间传递数据和触发事件,实现组件之间的交互和数据共享。中央总线可以被看作是一个中间人,负责接收和分发消息。这里要注意的是,我并没有把Subject进行分离,所以这并不是一个标准的Observer,但实现的思路基本是一致的,因为已经具备了所有的要素。
import IEvent from "./iEvent.ts";
class Event implements IEvent {
_events: Map<string, Function[]> = new Map();
getEvents(): Map<string, Function[]> {
return this._events;
}
getEvent(eventName: string): Function[] {
return this._events.get(eventName)!;
}
removeEvent(eventName: string) {
this._events.delete(eventName);
}
addEventListener(eventName: string, callback: Function) {
if (this._events.has(eventName)) {
this.removeEventListener(eventName, callback);
this._events.get(eventName)!.push(callback);
} else {
this._events.set(eventName, [callback]);
}
}
removeEventListener(eventName: string, callback: Function) {
if (this._events.has(eventName)) {
const callbacks = this._events.get(eventName)!;
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
}
//这是一个异步的事件触发器
emitEvent(eventName: string, ...args: any[]): void {
if (this._events.has(eventName)) {
const callbacks = this._events.get(eventName) || [];
new Promise((resolve) => {
callbacks.forEach((callback) => {
callback(...args)
})
resolve(void 0);
}).then(() => {
}).catch((e) => {
console.error(e);
})
}
}
//这是一个同步的事件触发器
emitEventSync(eventName: string, ...args: any[]): void {
if (this._events.has(eventName)) {
const callbacks = this._events.get(eventName) || [];
for (let i = 0; i < callbacks.length; i++) {
callbacks[i](...args)
}
}
}
//这是一个利用迭代器实现的可阻断的同步的事件触发器
emitEventIterable(eventName: string, ...args: any[]): boolean {
console.log('emitEventIterable', eventName, this._events.get(eventName))
if (this._events.has(eventName)) {
const callbacks = this._events.get(eventName) || [];
const eventIterable = function* () {
for (let i = 0; i < callbacks.length; i++) {
yield callbacks[i](...args);
}
}
const events = eventIterable()
while (true) {
const result = events.next();
if (result.value === false) {
return false;
}
if (result.done) {
return true;
}
}
}
return true;
}
}
export default Event;