3.8 自定义事件与通信机制
浏览器原生事件体系仅覆盖浏览器内置的交互与状态事件,自定义事件则允许开发者根据业务需求定义专属事件类型,配合事件驱动模式实现模块间、组件间的解耦通信。
自定义事件完全复用原生事件流机制,结合发布订阅模式封装的事件总线,是前端跨组件、跨模块通信的基础方案,也是事件驱动架构的核心载体。
3.8.1 自定义事件创建与触发
自定义事件通过原生CustomEvent构造函数创建,通过dispatchEvent方法触发,完全兼容原生事件的冒泡、捕获、取消机制,支持自定义传递业务数据,是实现组件内部事件、业务状态事件的基础。
一、基础语法
1. 创建自定义事件
使用 CustomEvent 构造函数,第一个参数为事件类型名,第二个参数为配置对象:
// 创建自定义事件 const myEvent = new CustomEvent('user-login', { bubbles: true, // 是否支持冒泡 cancelable: true, // 是否可取消 detail: { // 自定义数据,挂载在event.detail上 username: 'zhangsan', userId: '1001' } });2. 触发自定义事件
在指定元素上调用dispatchEvent触发事件,事件会按照事件流机制传播:
const button = document.querySelector('#loginBtn'); // 触发自定义事件 button.dispatchEvent(myEvent);3. 监听自定义事件
和原生事件完全一致,使用addEventListener监听:
document.addEventListener('user-login', (e) => { console.log('用户登录:', e.detail.username); });二、核心配置项
配置属性 | 类型 | 作用说明 |
| boolean | 事件是否支持冒泡机制,默认 |
| boolean | 事件是否可通过 |
| any | 自定义事件携带的数据,任意类型,挂载在事件对象的 |
三、核心特点
- 完全兼容原生事件机制:支持捕获、冒泡、取消,和原生事件使用方式完全一致,学习成本低。
- 数据透传能力:通过
detail属性可以携带任意业务数据,支持对象、数组、函数等所有类型。 - 事件语义化:可根据业务定义事件名称,如
order-submit、data-loaded,提升代码可读性。 - 灵活触发:可在任意元素、任意时机触发,不受浏览器原生交互限制。
四、取消自定义事件
当cancelable设为true时,监听函数中可调用preventDefault()取消事件,触发函数可通过返回值判断:
const submitEvent = new CustomEvent('before-submit', { cancelable: true }); // 监听中拦截 form.addEventListener('before-submit', (e) => { if (!form.value) { e.preventDefault(); // 取消事件 } }); // 触发时判断是否被取消 const isCanceled = !form.dispatchEvent(submitEvent); if (isCanceled) { console.log('提交已被拦截'); }3.8.2 事件总线基础实现
事件总线(Event Bus)是基于发布订阅模式(Pub/Sub)封装的通用事件通信工具,它独立于 DOM 元素,实现了纯逻辑层面的事件发布与订阅,是跨组件、跨模块通信的核心实现。
一、核心原理
事件总线维护一个事件-回调函数的映射表,提供订阅(on)、发布(emit)、取消订阅(off)、一次性订阅(once)四个核心方法,所有模块通过同一个事件总线实例通信,实现完全解耦。
二、基础实现代码
class EventBus { constructor() { // 存储事件与回调列表的映射:{ eventName: [fn1, fn2] } this.events = {}; } // 订阅事件 on(eventName, callback) { if (!this.events[eventName]) { this.events[eventName] = []; } this.events[eventName].push(callback); } // 发布事件 emit(eventName, ...args) { if (!this.events[eventName]) return; // 遍历执行所有订阅回调 this.events[eventName].forEach(callback => { callback(...args); }); } // 取消订阅 off(eventName, callback) { if (!this.events[eventName]) return; // 过滤掉要移除的回调 this.events[eventName] = this.events[eventName].filter( fn => fn !== callback ); } // 一次性订阅,触发后自动取消 once(eventName, callback) { const wrapper = (...args) => { callback(...args); this.off(eventName, wrapper); }; this.on(eventName, wrapper); } } // 全局事件总线实例 const eventBus = new EventBus();三、使用示例
// 模块A:订阅登录事件 eventBus.on('user:login', (userInfo) => { console.log('更新用户信息:', userInfo); }); // 模块B:发布登录事件 eventBus.emit('user:login', { username: 'lisi', role: 'admin' });四、核心特点
- 完全解耦:发布方和订阅方无需互相感知,也不依赖 DOM 层级,通过事件名匹配通信。
- 一对多通信:一个事件可以被多个订阅者同时监听,一次发布多方响应。
- 跨模块跨层级:不受组件层级、模块边界限制,全局任意位置都可发布订阅。
- 轻量灵活:无额外依赖,可根据业务扩展功能,如事件命名空间、通配符等。
3.8.3 跨组件通信基础
在组件化开发中,不同组件之间的通信是核心需求,基于自定义事件与事件总线,可以覆盖绝大多数跨组件通信场景,实现组件间的解耦交互。
一、常见通信场景与方案
通信场景 | 推荐方案 | 实现原理 |
父子组件通信 | 原生自定义事件 | 子组件触发自定义事件,父组件监听,基于事件冒泡机制 |
兄弟组件通信 | 全局事件总线 | 双方通过共同的事件总线实例发布订阅事件 |
跨层级组件通信 | 全局事件总线 | 绕过组件层级,直接通过事件总线通信 |
多组件状态同步 | 全局事件总线 | 状态变化时发布事件,所有依赖组件订阅更新 |
二、父子组件通信示例
基于 DOM 自定义事件的父子通信,是原生 Web Components 的标准通信方式:
// 子组件内部触发自定义事件,向父组件传值 class ChildComponent extends HTMLElement { connectedCallback() { this.querySelector('button').addEventListener('click', () => { // 触发自定义事件,冒泡到父组件 this.dispatchEvent(new CustomEvent('child-change', { bubbles: true, detail: { value: this.input.value } })); }); } } // 父组件监听子组件的自定义事件 class ParentComponent extends HTMLElement { connectedCallback() { this.addEventListener('child-change', (e) => { console.log('收到子组件数据:', e.detail.value); }); } }三、跨组件通信的最佳实践
- 事件命名规范:采用
模块:事件或组件:动作的命名方式,如user:logout、cart:update,避免命名冲突。 - 及时取消订阅:组件销毁、页面卸载时必须调用
off取消订阅,避免内存泄漏与重复触发。 - 合理选择方案:父子优先用原生自定义事件,跨层级用事件总线;避免滥用全局事件总线导致事件泛滥。
- 单向数据流原则:事件通信只做通知,数据通过参数传递,避免订阅方直接修改发布方数据,保持单向性。
四、方案边界
- 优势:实现简单、解耦性强、原生支持、无框架依赖。
- 局限:复杂多状态同步场景下,事件过多会导致维护成本上升,此时更适合引入专门的状态管理方案。