自定义事件:让代码之间也能“悄悄对话”

0 阅读5分钟

你有没有想过,除了浏览器自带的click、mouseover这些事件,我们能不能自己创造事件?比如“用户通关了”、“购物车满了”、“天气变热了”?今天我们就来学学自定义事件,让你能在代码的各个角落“放信号弹”,让其他模块“听到”后自动响应。

前言

想象一下,你是个指挥官,手下有侦察兵、炮兵、步兵。侦察兵发现敌情后,不能直接喊“开炮”,否则太乱。他需要一种机制——比如举起红旗——让炮兵看到红旗就开火,步兵看到红旗就隐蔽。这个“红旗”,就是自定义事件

在JS里,自定义事件就是让不同的模块通过“信号”通信,而不需要互相引用、直接调用。这样代码更松耦合,更好维护。

一、自定义事件的三种姿势

1. 最简单的:new Event()

new Event()创建一个基础事件对象,然后用dispatchEvent()触发。

// 创建一个自定义事件
const myEvent = new Event('myCustomEvent', {
  bubbles: true,   // 是否冒泡
  cancelable: true // 是否可取消默认行为
});

// 监听这个事件
document.addEventListener('myCustomEvent', (e) => {
  console.log('收到了自定义事件!');
});

// 触发事件
document.dispatchEvent(myEvent);

这个事件可以冒泡,可以取消,但它不能携带额外数据。

2. 能带数据的:new CustomEvent()

CustomEventEvent的升级版,它多了一个detail属性,可以用来传递数据。

const loginEvent = new CustomEvent('userLogin', {
  detail: {
    username: '张三',
    timestamp: Date.now()
  }
});

window.addEventListener('userLogin', (e) => {
  console.log(`${e.detail.username} 登录了,时间是${e.detail.timestamp}`);
});

window.dispatchEvent(loginEvent);

这个就厉害了,你想传啥就传啥——对象、数组、甚至函数都行。

3. 最古老的:document.createEvent()

这是老式写法,兼容IE等古董浏览器,但现在已经很少用了。了解一下就行:

const oldEvent = document.createEvent('Event');
oldEvent.initEvent('myEvent', true, true);
document.dispatchEvent(oldEvent);

日常开发,用CustomEvent就完事了。

二、实战:用自定义事件做模块通信

假设我们有一个游戏,玩家通关后,需要同时做三件事:播放胜利音乐、显示通关动画、记录分数。如果用传统方式,通关模块得分别调用音乐模块、动画模块、分数模块的方法,耦合度太高。

用自定义事件就优雅多了:

// 通关模块
function passLevel() {
  console.log('玩家通关了!');
  // 发信号
  const event = new CustomEvent('levelPassed', {
    detail: { level: 3, score: 1000 }
  });
  window.dispatchEvent(event);
}

// 音乐模块
window.addEventListener('levelPassed', (e) => {
  console.log(`播放第${e.detail.level}关胜利音乐`);
});

// 动画模块
window.addEventListener('levelPassed', (e) => {
  console.log(`播放通关烟花动画`);
});

// 分数模块
window.addEventListener('levelPassed', (e) => {
  console.log(`记录分数:${e.detail.score}`);
});

// 通关!
passLevel();

每个模块只关心自己该做的事,互不干扰。要加新功能?再加一个监听器就行,完全不用改动原有代码。

三、事件冒泡:自定义事件也能往上“飘”

如果自定义事件是在某个元素上触发的,它可以像原生事件一样冒泡,被父元素捕获。

<div id="parent">
  <button id="child">点我</button>
</div>
const child = document.getElementById('child');
const parent = document.getElementById('parent');

parent.addEventListener('customClick', (e) => {
  console.log('父元素收到了,事件目标:', e.target);
});

child.addEventListener('customClick', (e) => {
  console.log('子元素收到了');
  // 如果不阻止冒泡,父元素也会收到
});

const myEvent = new Event('customClick', { bubbles: true });
child.dispatchEvent(myEvent);

注意:CustomEvent默认也是可以冒泡的,只要bubbles: true

四、与原生事件的微妙差异

自定义事件和原生事件在使用上几乎一样,但有个本质区别:自定义事件不是浏览器自己产生的。这意味着:

  • 不会触发浏览器的默认行为(比如点击链接跳转)。
  • 不会影响页面的原生交互(比如输入框获得焦点)。
  • 在DevTools的Event Listeners面板里,可能不会显示(但现代浏览器基本都支持显示)。

五、应用场景:什么时候该用自定义事件?

1. 插件/组件通信

比如你写了一个轮播图插件,它内部可以派发slideChange事件,让外面知道当前切换到第几张了。

// 插件内部
this.dispatchEvent(new CustomEvent('slideChange', {
  detail: { index: currentIndex }
}));

// 使用者
carousel.addEventListener('slideChange', (e) => {
  console.log('现在展示第', e.detail.index, '张');
});

2. 松耦合架构

在大型应用中,不同模块之间最好不要互相知道对方的存在。自定义事件就是天然的“消息总线”。

// 购物车模块
function addToCart(item) {
  // 添加逻辑...
  window.dispatchEvent(new CustomEvent('cartUpdated', { detail: { item, count: newCount } }));
}

// 导航栏模块(显示购物车图标上的数字)
window.addEventListener('cartUpdated', (e) => {
  updateCartIcon(e.detail.count);
});

// 数据分析模块
window.addEventListener('cartUpdated', (e) => {
  trackEvent('add_to_cart', e.detail.item);
});

3. 跨组件、跨层级通信

在React/Vue等框架里,父子组件通信有props和emit,但爷孙组件、兄弟组件通信就麻烦些。这时候可以在公共父级或全局对象上派发自定义事件(当然,也可以用状态管理工具,但自定义事件是轻量选择)。

六、清理事件:别忘了“退订”

自定义事件也是事件,用完了要记得移除监听器,否则可能内存泄漏。

function handler(e) {
  console.log('收到了');
}

window.addEventListener('myEvent', handler);
// 某天不需要了
window.removeEventListener('myEvent', handler);

如果用的是匿名函数,是没法移除的,所以最好把监听器函数单独定义。

七、实战:一个简单的“全局事件总线”

我们可以封装一个极简的事件总线,方便在任何地方监听和触发:

const eventBus = {
  on(event, callback) {
    document.addEventListener(event, callback);
  },
  off(event, callback) {
    document.removeEventListener(event, callback);
  },
  emit(event, data) {
    document.dispatchEvent(new CustomEvent(event, { detail: data }));
  }
};

// 使用
eventBus.on('userLogin', (e) => {
  console.log('登录了', e.detail);
});
eventBus.emit('userLogin', { name: '张三' });

就这几行,你就有了一个全局通信工具。

八、总结:自定义事件让代码“会说话”

  • 创建事件new Event()(不带数据)或new CustomEvent()(带数据)
  • 触发事件:用dispatchEvent()派发
  • 监听事件:用addEventListener()接收
  • 冒泡:设置bubbles: true可以让事件向上传播
  • 应用场景:插件通信、松耦合架构、跨组件通信
  • 注意:用完后移除监听器,避免内存泄漏

自定义事件是JS里非常优雅的通信方式,它让不同模块之间可以“悄悄对话”,而不需要手拉手、硬耦合。掌握了它,你的代码会变得更灵活、更容易维护。

明天我们将进入MutationObserver的世界——一个能监听DOM变化的神器,当你想知道页面上的某个元素什么时候被添加、删除、修改属性时,它就是你最好的“卧底”。

如果你觉得今天的自定义事件够“传神”,点个赞让更多人看到。我们明天见!