应采用渐进式事件总线方案:在根容器设代理监听器捕获DOM事件并转发至EventBus,按ui:form:submit:login等分层命名规范 emit,支持通配订阅与模块化卸载,通过桥接器兼容旧代码,最终实现事件流统一管控。
用事件总线替代散落的 addEventListener
直接替换所有 addeventlistener 调用不现实,也不安全。更可行的路径是引入轻量级事件总线(event bus)作为中间层,让旧代码继续注册监听,但底层统一走总线分发。这样既避免大规模修改,又能逐步收口——所有 dom 事件先由少量代理监听器捕获,再转发给总线,业务逻辑侧只与总线交互。
建立 DOM 事件代理网关
在根容器(如 #app 或 document.body)上挂载少数几个委托监听器,按语义分类捕获事件:
- 点击类:监听
click,用event.target.matches('[data-event]')匹配带标记的元素 - 表单类:监听
input、change、submit,通过name、id或自定义data-action提取意图 - 键盘/焦点类:监听
keydown或focusin,结合event.target.dataset.hotkey等做路由
每个代理监听器收到原生事件后,不做具体处理,而是调用 EventBus.emit('ui:click', { target, detail }) 向上抛出标准化事件对象。
定义可寻址的事件命名与上下文规范
事件名不是随意拼的字符串,而是分域、分层、可检索的地址式命名:
-
ui:form:submit:login—— 表示登录表单提交 -
api:response:user:loaded—— 表示用户数据加载完成 -
state:cart:item:added—— 表示购物车新增商品
配套提供 EventBus.on('ui:form:submit:*', handler) 通配支持,并允许按模块前缀批量注销:EventBus.off('ui:form:submit:')。所有 handler 接收统一结构:{ type, payload, meta: { source, timestamp, originEl } }。
渐进迁移策略与兼容桥接
不强求一次性清理,而是设计双模运行期:
- 新模块只调用
EventBus.on(...),不再写addEventListener - 遗留模块仍可用原生方式注册,但通过一个“桥接器”自动将其转为总线订阅:
patchLegacyListeners(document.getElementById('old-widget'))—— 扫描其内部所有addEventListener并重绑定到总线 - 构建 ESLint 规则禁止新增
addEventListener,推动团队转向总线 API
收口完成后,DOM 代理层成为唯一事件入口,事件流向、触发时机、错误隔离、调试追踪全部可控。重构不是消灭旧代码,而是让它运行在新的治理边界里。











