JS `EventTarget` 与自定义事件:构建可扩展的事件系统

咳咳,各位靓仔靓女们,今天老衲要给大家讲讲JS里“EventTarget”这位老大哥,以及如何利用它打造属于你自己的、可扩展的事件系统。保证听完之后,你的代码就像穿了Prada,倍儿有面儿!

第一章:EventTarget——事件世界的基石

首先,咱们来认识一下EventTarget。这玩意儿就像是所有能“发射”和“接收”事件的物体的祖宗。想想看,DOM元素(比如<div><button>)能监听click事件,这都要归功于它们继承了EventTarget的特性。

但EventTarget不仅仅服务于DOM。它更像是一个通用的事件机制,可以让你创造任何你想要的事件发射器。

1.1 什么是EventTarget?

简单来说,EventTarget是一个接口,定义了三个核心方法:

  • addEventListener(type, listener, options):注册一个特定类型的事件监听器。
  • removeEventListener(type, listener, options):移除一个特定类型的事件监听器。
  • dispatchEvent(event):触发一个事件。

这三个方法撑起了整个事件系统的骨架。

1.2 EventTarget的实现

EventTarget本身是个接口,不能直接实例化。但是,你可以通过继承或者组合的方式,在你的类中实现EventTarget的功能。

最简单的方式是创建一个类,并手动实现这三个方法。 这样做让你对事件处理流程有完全的控制权。

第二章:手撸一个简易版EventTarget

为了更深入地理解EventTarget的工作原理,咱们先撸一个简易版,麻雀虽小,五脏俱全。

class MyEventTarget {
  constructor() {
    this._listeners = {}; // 用来存储事件监听器
  }

  addEventListener(type, listener) {
    if (!this._listeners[type]) {
      this._listeners[type] = [];
    }
    this._listeners[type].push(listener);
  }

  removeEventListener(type, listener) {
    if (!this._listeners[type]) {
      return;
    }

    this._listeners[type] = this._listeners[type].filter(
      (existingListener) => existingListener !== listener
    );
  }

  dispatchEvent(event) {
    if (!this._listeners[event.type]) {
      return;
    }

    this._listeners[event.type].forEach((listener) => {
      listener.call(this, event); // 注意:这里用call改变了this指向
    });
  }
}

// 举个栗子
const myTarget = new MyEventTarget();

function handleClick(event) {
  console.log('事件类型:', event.type);
  console.log('事件目标:', event.target);
}

myTarget.addEventListener('click', handleClick);

const clickEvent = { type: 'click', target: myTarget };
myTarget.dispatchEvent(clickEvent); // 输出:事件类型: click  事件目标: [MyEventTarget]
myTarget.removeEventListener('click', handleClick);
myTarget.dispatchEvent(clickEvent); // 啥也不输出,因为监听器被移除了

这段代码实现了一个基本的事件目标。_listeners对象用于存储不同事件类型对应的监听器数组。addEventListener用于添加监听器,removeEventListener用于移除监听器,dispatchEvent用于触发事件。

第三章:构建可扩展的事件系统

有了EventTarget的基础,咱们就可以构建更复杂的事件系统了。 核心在于设计好事件的类型、事件的数据结构,以及事件的触发方式。

3.1 定义事件类型

事件类型是事件系统的灵魂。要确保事件类型清晰、明确,并且易于理解。

可以采用常量或者枚举的方式来定义事件类型:

// 使用常量
const EVENT_TYPE_LOGIN = 'login';
const EVENT_TYPE_LOGOUT = 'logout';
const EVENT_TYPE_DATA_CHANGE = 'dataChange';

// 或者使用枚举 (TypeScript)
// enum EventType {
//   LOGIN = 'login',
//   LOGOUT = 'logout',
//   DATA_CHANGE = 'dataChange'
// }

3.2 设计事件数据结构

事件数据是事件携带的信息。要根据事件的类型,设计合理的数据结构。

class CustomEvent {
  constructor(type, detail) {
    this.type = type;
    this.detail = detail; // 事件携带的数据
    this.target = null; // 事件目标
  }
}

// 举个栗子
const loginEvent = new CustomEvent(EVENT_TYPE_LOGIN, { username: 'zhangsan' });

3.3 封装事件触发

为了方便触发事件,可以封装一个trigger方法。

class MyEventTarget {
  // ... (之前的代码)

  trigger(type, detail) {
    const event = new CustomEvent(type, detail);
    event.target = this;
    this.dispatchEvent(event);
  }
}

// 举个栗子
const myTarget = new MyEventTarget();

function handleLogin(event) {
  console.log('用户登录:', event.detail.username);
}

myTarget.addEventListener(EVENT_TYPE_LOGIN, handleLogin);

myTarget.trigger(EVENT_TYPE_LOGIN, { username: 'lisi' }); // 输出:用户登录: lisi

3.4 实现事件的传播

有时候,我们需要事件在多个对象之间传播,就像DOM事件的冒泡和捕获一样。

class EventSystem {
  constructor() {
    this._targets = []; // 存储所有事件目标
  }

  register(target) {
    this._targets.push(target);
  }

  unregister(target) {
    this._targets = this._targets.filter((t) => t !== target);
  }

  dispatch(event) {
    // 模拟事件冒泡
    let currentTarget = event.target;
    while (currentTarget) {
      if (currentTarget._listeners && currentTarget._listeners[event.type]) {
        currentTarget._listeners[event.type].forEach((listener) => {
          listener.call(currentTarget, event);
        });
      }
      currentTarget = currentTarget.parentNode; // 假设每个对象都有parentNode属性
      if(!currentTarget){
          break;
      }
    }
  }
}

// 举个栗子
class Node {
  constructor(name) {
    this.name = name;
    this.parentNode = null;
    this._listeners = {};
  }
  addEventListener(type, listener) {
    if (!this._listeners[type]) {
      this._listeners[type] = [];
    }
    this._listeners[type].push(listener);
  }

  removeEventListener(type, listener) {
    if (!this._listeners[type]) {
      return;
    }

    this._listeners[type] = this._listeners[type].filter(
      (existingListener) => existingListener !== listener
    );
  }

  dispatchEvent(event) {
    event.target = this;
    eventSystem.dispatch(event);
  }
}

const eventSystem = new EventSystem();

const node1 = new Node('node1');
const node2 = new Node('node2');
const node3 = new Node('node3');

node1.parentNode = node2;
node2.parentNode = node3;

function handleEvent(event) {
  console.log(`事件类型: ${event.type}, 目标: ${event.target.name}`);
}

node3.addEventListener('customEvent', handleEvent);

const customEvent = new CustomEvent('customEvent', { message: 'Hello' });
node1.dispatchEvent(customEvent); // 输出:事件类型: customEvent, 目标: node3
                                   // 因为事件从node1冒泡到node3

3.5 EventTarget的继承与组合

在实际开发中,你可能需要让你的类拥有EventTarget的功能,有两种方式可以实现:

  • 继承: 如果你的类不需要继承其他的类,那么直接继承 MyEventTarget 是最简单的方式。
  • 组合: 如果你的类已经继承了其他的类,那么可以使用组合的方式,将 MyEventTarget 的实例作为你的类的一个属性,然后将EventTarget的方法代理到你的类上。
// 继承
class MyComponent extends MyEventTarget {
  constructor() {
    super();
    this.name = 'MyComponent';
  }

  doSomething() {
    this.trigger('action', { message: 'Doing something' });
  }
}

// 组合
class AnotherComponent {
  constructor() {
    this.eventTarget = new MyEventTarget();
    this.name = 'AnotherComponent';
  }

  addEventListener(type, listener) {
    this.eventTarget.addEventListener(type, listener);
  }

  removeEventListener(type, listener) {
    this.eventTarget.removeEventListener(type, listener);
  }

  dispatchEvent(event) {
    this.eventTarget.dispatchEvent(event);
  }

  trigger(type, detail) {
    this.eventTarget.trigger(type, detail);
  }

  doSomethingElse() {
    this.trigger('anotherAction', { message: 'Doing something else' });
  }
}

第四章:EventTarget的应用场景

EventTarget的应用场景非常广泛,只要你需要一个灵活的事件机制,都可以考虑使用它。

应用场景 描述
组件通信 在复杂的应用中,不同的组件之间需要相互通信,可以使用EventTarget来构建一个松耦合的通信机制。
状态管理 可以使用EventTarget来通知状态的变化,例如,当用户登录或者退出时,触发相应的事件。
自定义UI控件 如果你需要创建自定义的UI控件,可以使用EventTarget来处理用户的交互事件,例如,按钮的点击事件、滑块的滑动事件等。
游戏开发 在游戏开发中,可以使用EventTarget来处理游戏事件,例如,角色的移动、攻击、死亡等。
异步任务管理 可以使用EventTarget来通知异步任务的完成或者失败,例如,当一个网络请求完成时,触发一个requestComplete事件,当一个文件上传完成时,触发一个uploadComplete事件。

第五章:高级技巧与注意事项

  • 避免内存泄漏: 确保在不再需要监听器时,及时移除它们,否则可能会导致内存泄漏。
  • 合理使用事件委托: 对于大量的同类型元素,可以使用事件委托来减少监听器的数量,提高性能。
  • 事件命名规范: 保持事件名称的一致性和可读性,方便维护和调试。
  • 使用once选项: addEventListeneroptions参数可以传入{ once: true },让监听器只执行一次。
myTarget.addEventListener('specialEvent', () => {
  console.log("This will only be logged once!");
}, { once: true });

myTarget.trigger('specialEvent', {});
myTarget.trigger('specialEvent', {}); // 不会再次触发
  • 使用capturepassive选项: capture用于设置事件捕获,passive用于优化滚动性能。

第六章:总结

EventTarget是构建可扩展事件系统的强大工具。通过理解其核心概念和方法,并结合实际场景,你可以打造出灵活、高效的事件机制,让你的代码更加健壮、易于维护。记住,好的事件系统就像一个优秀的媒婆,能让不同的组件和谐相处,共同构建美好的未来。

好了,今天的讲座就到这里。希望大家有所收获,早日成为代码界的Prada代言人! 下课! 记得点赞!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注