shadow dom 默认阻断事件冒泡,仅当事件的 composed 属性为 true 时才能穿透边界;原生事件如 click 默认 composed: false,自定义事件需显式设置 composed: true 才能跨边界传播。

Shadow DOM 会阻断事件冒泡穿过其边界,但并非所有事件都被完全拦截——关键取决于事件是否被设置为 可穿透(composed: true)。
事件能否穿出 Shadow DOM 取决于 composed 属性
原生浏览器事件默认 不穿透 Shadow DOM 边界。例如 click、input、keydown 等内置事件的 composed 属性为 false,因此它们在到达 shadow root 时就会停止冒泡,不会继续向 light DOM 传播。
- 若手动派发自定义事件,需显式设置
composed: true才能跨边界传播:
const event = new CustomEvent('my-event', { bubbles: true, composed: true });<br>shadowRoot.dispatchEvent(event);
此时该事件可在 shadow root 外的 light DOM 中被监听到;若 omit composed: true(或设为 false),则仅在 shadow 内部冒泡。
事件目标与事件路径在 Shadow 边界处被截断
即使事件未穿透,event.target 和 event.composedPath() 的行为也不同:
-
event.target始终返回原始触发节点(可能在 shadow 内部); -
event.composedPath()返回“扁平化”后的路径:对非 composed 事件,路径止于 shadow root;对 composed 事件,路径会包含 shadow root 外的祖先节点。
这意味着监听器无法通过 e.target 直接判断事件来源是否在 shadow 内——需要结合 e.composedPath()[0] 或检查 e.target.getRootNode() !== document 来识别。
light DOM 监听器收不到非 composed 事件,但可通过代理捕获
如果希望 light DOM 感知 shadow 内部的用户交互(如按钮点击),有几种常用做法:
- 在 shadow 内部主动 dispatch 一个
composed: true的自定义事件; - 在 shadow host 元素上监听原生事件(如
click),因 shadow host 是 light DOM 节点,且事件在 shadow 内触发后若冒泡至 host,仍属于同一树内传播(不跨 boundary); - 避免依赖跨 boundary 冒泡,改用属性/事件总线/Context API 等更可控的通信方式。
注意:focus、scroll 等部分事件有特殊行为
某些事件天生不冒泡(如 focus),或仅在特定条件下穿透(如 focusin/focusout 默认 composed: true);scroll 事件默认不冒泡也不穿透。实际使用中应查阅 MDN 对具体事件的 composed 默认值说明,不可一概而论。











