要让自定义事件穿越 Shadow DOM 边界,必须设置 composed: true 且通常配合 bubbles: true;仅 bubbles: true 不足,composed: true 才允许事件从 shadow root 穿出至宿主及外部 DOM。
要让自定义事件真正穿越 shadow dom 边界,必须显式设置 composed: true,且通常需与 bubbles: true 配合使用。仅设 bubbles: true 不足以穿透——它只控制事件在当前树内冒泡;composed: true 才是打开“跨边界通行许可”的关键开关。
为什么 composed 是决定性参数
composed 决定事件能否被外部 light DOM 或父级组件捕获。Shadow DOM 默认将事件封装在内部,即使事件冒泡到 shadow root 顶层,也会在此终止。设为 true 后,事件才被允许“重定向”至宿主元素之外,进入常规 DOM 树。
-
bubbles: true→ 事件能从子节点向上冒泡到 shadow root -
composed: true→ 事件能从 shadow root 穿出,抵达宿主元素、父容器乃至 document - 二者缺一不可:没有
bubbles,事件连 shadow root 都到不了;没有composed,就卡在 shadow root 出不去
正确派发方式(代码要点)
在自定义元素内部,必须通过组件实例(而非 shadowRoot)调用 dispatchEvent,并传入含两个标志的 CustomEvent:
- ✅ 正确:
this.dispatchEvent(new CustomEvent('notify', { bubbles: true, composed: true, detail: {...} })) - ❌ 错误:
this.shadowRoot.dispatchEvent(...)—— 这会丢失上下文,无法正确触发 composed 行为 - ❌ 错误:
new CustomEvent('notify', { bubbles: true })—— 缺少composed: true,事件止步于 shadow boundary
多层嵌套时的监听位置
只要事件设置了 composed: true,监听方无需特殊操作,直接在期望接收的节点上添加原生事件监听器即可:
- 若想让最外层页面捕获,用
document.addEventListener('notify', ...) - 若由中间组件接收并处理,用
outerEl.addEventListener('notify', ...)(绑定在宿主元素上) - 不要在
shadowRoot上监听该事件——它本就不是为内部接收设计的
验证是否生效的小技巧
在事件回调中打印 e.composedPath(),观察路径是否包含 shadow host 及其外部祖先:
- 有
composed: true:路径类似[button, #shadow-root, my-card, body, html, document] - 无
composed: true:路径止步于[button, #shadow-root],不出现宿主或更高层节点 - 也可在 DevTools 的 Event Listeners 面板中检查目标节点是否注册了对应事件监听器

















