手把手教你实现高效的自定义事件机制

FSD-国曼 前端 阅读 2,278
赞 3 收藏
二维码
手机扫码查看
反馈

我又在自定义事件上踩坑了

上周改一个老项目的状态通知逻辑,原本用的是简单的回调函数传数据,结果越改越乱,父子组件之间传来传去,最后我干脆重构了一把,上了自定义事件。但问题是,方案太多:EventTarget、mitt、CustomEvent DOM 事件、还有 Vuex/Redux 那种发布订阅……我试了几个,发现差别比我想的大得多。

手把手教你实现高效的自定义事件机制

最终我选了 mitt,不是因为它最强大,而是它最省事。下面说说我的真实体验,哪些能用,哪些真坑。

谁更灵活?谁更省事?

先亮结论:如果你只是想在组件间传个状态、通知一下某个动作完成了,别折腾 EventTarget 或自己实现发布订阅,直接上 mitt。它轻、快、兼容性好,而且 API 简洁到不行。

我自己写过一个基于 EventTarget 的全局事件总线,看起来挺正经:

class EventBus extends EventTarget {
  emit(type, detail) {
    this.dispatchEvent(new CustomEvent(type, { detail }));
  }

  on(type, callback) {
    this.addEventListener(type, (e) => callback(e.detail));
  }

  off(type, callback) {
    this.removeEventListener(type, (e) => callback(e.detail));
  }
}

const bus = new EventBus();

用起来也还行:

// 组件 A
bus.on('user-login', (user) => {
  console.log('用户登录了:', user);
});

// 组件 B
bus.emit('user-login', { id: 1, name: '张三' });

但问题来了——off 方法根本解绑不了。因为我在 addEventListener 里用了箭头函数封装,导致 removeEventListener 找不到原函数。这个坑我踩了两次,第二次还是忘了。

你得改成这样:

on(type, callback) {
  const wrapper = (e) => callback(e.detail);
  this._handlers = this._handlers || {};
  this._handlers[${type}] = wrapper;
  this.addEventListener(type, wrapper);
}

off(type) {
  const wrapper = this._handlers?.[type];
  if (wrapper) {
    this.removeEventListener(type, wrapper);
  }
}

这代码已经开始变味了,本来想搞个简单事件总线,结果要维护 handler 映射,还得考虑内存泄漏。这时候我就问自己:我真的需要这么重的方案吗?

mitt:小而美才是王道

后来我换了 mitt,npm install mitt 一下,十几行代码搞定:

import mitt from 'mitt';

const bus = mitt();

// 监听
bus.on('user-login', (user) => {
  console.log('登录用户:', user);
});

// 触发
bus.emit('user-login', { id: 1, name: '张三' });

// 解绑
bus.off('user-login', handler);

干净利落,支持通配符(虽然我没用),支持 all 订阅,压缩后不到 200 行代码。关键是——没有依赖,不绑定 DOM,SSR 友好,TypeScript 支持原生。

我在 Next.js 项目里也用了,完全没问题。不像 CustomEvent,只能在浏览器环境用,服务端渲染直接报错。

CustomEvent:听着高大上,其实限制多

有人喜欢用原生的 CustomEvent 做组件通信,尤其是在 Web Components 里,确实有它的场景。比如你在做一个独立的 UI 组件库,想通过 DOM 事件往外抛状态,那用 CustomEvent 是标准做法。

// 在自定义元素内部
const event = new CustomEvent('load-complete', {
  detail: { loaded: true },
  bubbles: true,
  composed: true
});
this.dispatchEvent(event);
<!-- 使用时 -->
<my-loader @load-complete="handleLoad"></my-loader>

这种写法在 Vue 或 Lit 里很常见,但注意:bubbles 和 composed 必须设对,否则跨 Shadow DOM 传不过去。我之前没加 composed,调试了半小时才发现是事件穿不过 shadow boundary。

而且 CustomEvent 是 DOM 相关的,你不能在 service worker 或 node 环境用。如果你想做全局状态通知,比如“用户登出”要刷新多个模块,用 CustomEvent 就显得特别别扭——你总不能 document.body.dispatchEvent(‘logout’) 吧?这也太怪了。

Redux Action + Middleware?杀鸡用牛刀

有同事坚持用 Redux dispatch 一个 action 来通知事件,比如:

dispatch({ type: 'USER_LOGIN_SUCCESS', payload: user });

然后一堆 reducer 和 middleware 响应。我说你这是为了发个通知,启动了一个核反应堆。除非你已经在用 Redux 管理全局状态,否则纯为了事件通信引入 Redux,成本太高了。

而且 action 是同步的(默认),你要做副作用还得套 thunk 或 saga,复杂度飙升。我现在的态度是:状态管理归状态管理,事件通知归事件通知,别混在一起。

我的选型逻辑

我现在是怎么选的?看场景:

  • 组件间轻量通信(非父子) → 用 mitt。我已经把它加到公司脚手架的默认依赖里了。
  • Web Components 内部对外暴露事件 → 用 CustomEvent,标准、兼容性好。
  • DOM 操作相关,比如滚动、点击组合 → 自定义事件挂载在元素上,用 addEventListener 接。
  • 全局状态变更通知 → 如果用了 Pinia/Vuex/Redux,可以用 store 的 $onAction 或 middleware;否则还是 mitt 更直接。
  • 别自己造轮子写 EventTarget 总线,除非你真的需要和浏览器事件系统打通,而且愿意处理解绑陷阱。

至于有些人提的 RxJS Subject,我也试过。BehaviorSubject 确实功能强,支持缓存、异步、取消订阅,但 learning curve 太陡。一个小项目里为几个事件引入 RxJS,我觉得不值得。等你真需要处理复杂的事件流(比如防抖、合并、切换)时再上也不迟。

性能对比:差距比我想象的小

我本来以为 mitt 会比原生 EventTarget 慢,毕竟多了层封装。结果我用 10 万次 emit 做了简单 benchmark:

console.time('mitt');
for (let i = 0; i < 100000; i++) {
  bus.emit('test', { n: i });
}
console.timeEnd('mitt');

console.time('EventTarget');
for (let i = 0; i < 100000; i++) {
  target.dispatchEvent(new CustomEvent('test', { detail: { n: i } }));
}
console.timeEnd('EventTarget');

结果:mitt 反而快了 10% 左右。可能是 CustomEvent 创建实例的开销更大。当然这测试不严谨,但在日常使用中,这点性能差异可以忽略。代码清晰度和可维护性更重要。

踩坑提醒:这三点一定注意

最后分享三个我踩过的坑:

  1. 记得解绑事件,尤其是 mitt 在 SPA 里用的时候。如果在组件 mounted 里 on,但没在 unmounted 时 off,容易内存泄漏。Vue 3 setup 中可以用 onUnmounted 注册解绑。
  2. 事件名别瞎写,建议统一前缀。比如都用 user:login、modal:open 这种格式,避免冲突。我之前两个模块都用了 save-success,debug 半天才发现。
  3. 不要在事件里传复杂对象(比如 DOM 元素、函数),序列化会有问题。如果要用,确保接收方在同一上下文。跨 iframe 通信时尤其要注意。

还有一个冷知识:你可以用 fetch(‘https://jztheme.com/api/event’) 发个日志请求来 debug 事件触发,虽然不推荐线上用,但临时测流程挺方便。

以上是我的对比总结

说实话,没有完美的方案。我目前主推 mitt,不是因为它技术最先进,而是它让我少操心。开发这么多年,越来越觉得:能跑、好维护、别人接手不骂你,才是好代码。

如果你现在项目里还在用回调地狱传状态,或者满屏的 props drilling,不妨试试 mitt。十几行代码的事,可能就救了你的架构。

以上是我踩坑后的总结,希望对你有帮助。有不同看法欢迎评论区交流,比如你坚持用 EventTarget 有啥高招,我也想学学。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论