shadow dom 事件默认不穿透到 light dom,需同时设置 bubbles: true 和 composed: true 才能跨边界冒泡;原生事件天生 composed: false,无法穿透,只能手动派发新事件;事件监听位置和 composed 属性共同决定是否可接收。

Shadow DOM 事件默认不穿透到 Light DOM
Shadow DOM 的事件默认不会冒泡到宿主元素外的 Light DOM,这是设计初衷——封装隔离。比如你在 shadowRoot 里点击一个按钮,document.addEventListener('click', ...) 拿不到这个事件,除非显式配置。
常见错误现象:绑在父容器上的事件监听器收不到 Shadow 内部触发的 click、input 等,以为“没触发”,其实是被拦截了。
- 原生事件(如
click、keydown)默认bubbles: true,但在 Shadow 边界处被截断 - 自定义事件需手动设
bubbles: true且composed: true才能穿透 -
composed: false(默认值)时,事件连宿主元素(host)的监听器都收不到
让事件穿透到 Light DOM:必须设 composed: true
只有同时满足 bubbles: true 和 composed: true,事件才能跨过 Shadow boundary 向上冒泡到 Light DOM。
示例:在 Shadow 内派发一个可穿透的自定义事件
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
const event = new CustomEvent('my-action', {
bubbles: true,
composed: true, // 关键!缺它就出不去
detail: { value: 'done' }
});
this.shadowRoot.querySelector('button').dispatchEvent(event);
此时 Light DOM 中的 host.addEventListener('my-action', ...) 或更外层的 document.addEventListener('my-action', ...) 都能收到。
-
composed: true是唯一决定“能否跨 boundary”的开关,bubbles只控制是否向上冒泡(在同域内) - 原生事件(如
click)无法通过 JS 修改其composed属性,它们天生composed: false,所以永远不穿透 - 想让原生事件“透出”,只能在 Shadow 内手动
dispatch一个新事件,并设composed: true
事件监听器位置影响接收范围
事件能否被监听到,取决于你把 addEventListener 挂在哪,以及事件是否 composed。
- 挂载在
host上:能收到composed: true的事件,也能收到 Shadow 内直接 dispatch 到 host 的事件(不依赖冒泡) - 挂载在
shadowRoot上:只收 Shadow 内部事件,无论composed值 - 挂载在
document或 Light DOM 父级:仅当事件composed: true且bubbles: true才能到达 - 使用
useCapture: true不改变composed限制,capture 阶段也收不到未合成的事件
调试技巧:用 event.composed 和 event.target 判断来源
收到事件时,别急着处理,先看两个关键属性:
-
event.composed返回true表示它穿过了 Shadow boundary -
event.target在composed: true事件中,可能指向 Shadow 内部元素(如#text或div),但event.composedPath()才反映真实传播路径 - 注意:
event.target在穿透后会被重写为host(不是原始内部元素),这是 Shadow DOM 的“目标重定向”行为;要用event.composedPath()[0]拿原始触发源
容易忽略的一点:即使事件成功穿透,event.target 也不再是 Shadow 里的那个按钮,而是宿主元素——这是规范行为,不是 bug。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










