🌺
摘要
用 window 作为轻量事件总线:构造 CustomEvent 并 dispatchEvent,配合 addEventListener 在模块、微前端或跨 iframe 同页场景里解耦通信;注意同步派发与 preventDefault 的返回值。
在浏览器中,事件不仅来自点击、键盘等用户操作,也可以用脚本主动派发。window.dispatchEvent(event) 表示在全局 Window 对象上触发一个事件:凡是挂在 window 上的监听器(捕获/冒泡阶段)都有机会响应。配合 CustomEvent,可以把 window 当作轻量的同页事件总线,在互不引用的模块之间传递信号与载荷。
本文聚焦 window.dispatchEvent;同一套 API 也适用于 document、任意 Element 等,因为它们都继承自 EventTarget。
1. 最小示例
// 监听
window.addEventListener("app:ready", (e) => {
console.log(e.detail); // { version: "1.0" }
});
// 派发
window.dispatchEvent(
new CustomEvent("app:ready", {
detail: { version: "1.0" },
}),
);
CustomEvent的第二个参数里,detail是业务自定义的附加数据(任意可结构化克隆的类型更稳妥,见下文注意点)。- 事件名习惯上用命名空间式字符串(如
app:theme-change),减少与将来浏览器内置事件撞名。
MDN 参考:Window.dispatchEvent、CustomEvent。
2. dispatchEvent 的返回值
const ok = window.dispatchEvent(event);
- 若事件
cancelable: true,且某个监听器里调用了event.preventDefault(),则dispatchEvent返回false;否则为true。 - 不可取消的事件(
cancelable: false)派发后通常返回true(除非派发被中止等异常情况)。
这可以用来做「可否决」的全局流程,例如发布前校验:
window.addEventListener(
"app:before-navigate",
(e) => {
if (hasUnsavedChanges()) e.preventDefault();
},
{ passive: false },
);
const ev = new CustomEvent("app:before-navigate", { cancelable: true });
if (!window.dispatchEvent(ev)) {
// 被某处 preventDefault,取消导航
}
注意:监听器若需要调用 preventDefault,不能在该监听器上使用 { passive: true }(被动监听器会忽略 preventDefault)。
3. CustomEvent 常用选项
new CustomEvent("my:event", {
detail: { userId: 42 },
bubbles: true, // 是否冒泡(对 window 派发时,冒泡到 window 即顶层,意义主要在 document / 元素链上)
cancelable: true, // 是否允许 preventDefault 使 dispatchEvent 返回 false
composed: true, // 是否穿过 Shadow DOM 边界(与 Web Components 一起用时重要)
});
bubbles:为true时,事件会沿 DOM 树冒泡。在window上派发时,目标已是顶层,冒泡行为对「只监听window」的场景影响不大;若改为在document.body等元素上派发并希望事件最终能被window听到,通常需要bubbles: true(并正确理解捕获/冒泡顺序)。composed:与 Shadow DOM 配合时,若要在宿主文档侧监听影子树内派发的自定义事件,派发端常设composed: true。
4. 同步执行:顺序与性能
dispatchEvent 是同步的:从派发一刻起,浏览器会立即按路径调用相关监听器,全部执行完后,dispatchEvent 才返回。
影响包括:
- 任意监听器里的异常若未捕获,可能中断后续逻辑;重要链路可用
try/finally或各自监听里 try/catch。 - 不要在监听器里做重计算或长时间任务,以免阻塞主线程;可改为监听器里
queueMicrotask/requestAnimationFrame/ 丢给 Worker。
5. detail 里放什么
- 推荐:普通对象、字符串、数字等;避免循环引用。
- 注意:
detail不是为跨线程设计的;若未来需要 structured clone(例如postMessage),应避免传递不可克隆对象(如部分 DOM 节点、函数),以免迁移通道时踩坑。 - 只读约定:部分团队约定监听方不修改
e.detail,由派发方保证不可变或拷贝后再读,减少隐式耦合。
6. 与 new Event 的区别
原生 Event 没有标准的 detail 字段;自定义载荷应使用 CustomEvent。
window.dispatchEvent(new Event("simple", { bubbles: true }));
适合无载荷、仅表达「发生了某件事」的信号。
7. 常见使用模式
7.1 同页多 bundle / 微前端之间的松耦合
各子应用不互相 import,约定全局事件名与 detail 形状:
// 子应用 A:主题变更
window.dispatchEvent(
new CustomEvent("shell:theme", { detail: { mode: "dark" } }),
);
// 子应用 B:订阅
window.addEventListener("shell:theme", (e) => applyTheme(e.detail.mode));
7.2 与业务状态层配合
状态管理库更新 store 后,需要通知非框架代码(如某段遗留 jQuery 插件)时,可派发一次自定义事件作为适配层,而不是让插件轮询 DOM。
7.3 一次性通知
function once(name, handler) {
const fn = (e) => {
handler(e);
window.removeEventListener(name, fn);
};
window.addEventListener(name, fn);
}
8. 和「原生事件」的对比(概念上)
- 用户触发的
click、keydown等由浏览器合成;dispatchEvent则是脚本模拟,可信模型不同(例如不会自动带上用户手势带来的某些权限行为)。 - 不要随意派发安全敏感语义的事件名去「骗」第三方脚本;自定义事件应限定在明确约定的契约内。
9. 小结
| 要点 | 说明 |
|---|---|
| API | target.dispatchEvent(event),target 可为 window |
| 自定义载荷 | 优先 new CustomEvent(name, { detail }) |
| 可取消 | cancelable: true + 监听里 preventDefault() → dispatchEvent 返回 false |
| 执行模型 | 同步调用监听器,注意性能与异常 |
| Shadow DOM | 需要穿出影子树时关注 composed |
在需要解耦、可测试、无强依赖的同页通信时,window + CustomEvent + dispatchEvent 往往比全局可变单例更干净;规模变大时再引入专用消息总线或状态库也不迟。
文章发表于 2026/05/07 09:36