Shadow DOM 内部产生的事件,默认无法穿越影子边界——影子边界如同一道防火墙,将事件封锁在组件内部。若想让外部容器成功监听,必须在创建事件时明确添加 composed: true。此时你会发现,event.target 指向的是宿主元素(比如 ),而真实的触发源则隐藏在 event.composedPath()[0] 中。使用 dispatchEvent() 手动派发事件时,务必记得声明 composed: true;原生事件(如 click、input)默认 composed 为 true,但这仅限于浏览器自动触发的场景——当你用 dispatchEvent() 主动派发时,它并不会自动为你补上这个属性。

事件默认不穿透边界,composed: true 是跨 Shadow Boundary 的关键
Shadow DOM 内部触发的事件(比如点击一个 )通常不会冒泡到宿主元素之外。事件被重定向后,event.target 显示为宿主元素(如 ),但原始触发节点仍藏在 event.composedPath()[0] 中。如果你需要外部监听该事件(例如父组件响应子按钮的点击),就必须显式设置 composed: true:
new CustomEvent('click', { bubbles: true, composed: true })—— 缺少composed: true,外部的addEventListener永远无法收到- 原生事件(如
click、input)默认composed为true,但仅限于浏览器原生触发;通过dispatchEvent()手动派发时,必须显式声明 - IE 完全不支持
composed属性,若需兼容低版本浏览器,可借助 polyfill 或改用属性变更结合attributeChangedCallback进行间接通信
event.composedPath() 是获取真实触发源的唯一可靠方式
在宿主元素上监听事件时,event.target 始终返回宿主本身(如 ),而非内部按钮。要想获取真正被点击的节点,只能依赖 event.composedPath():
event.composedPath()[0]即原始触发元素(例如 Shadow 内部的)- 该数组仅在事件冒泡阶段有效,捕获阶段无法使用
- 避免使用
event.path—— 这是 Chrome 私有属性,Firefox 和 Safari 行为不一致,且已被标记为废弃 - 如需判断事件是否来自 Shadow 内部,可结合
event.composedPath().includes(this.shadowRoot)进行检查
不要依赖宿主上的 event.target 进行 click 逻辑分支
不少开发者习惯在自定义元素中写 this.addEventListener('click', e => { if (e.target.matches('button')) {...} }),这在 Shadow DOM 环境下会失效:
e.target永远是,无法匹配'button'选择器- 正确做法:在 shadow root 内部绑定事件,例如
this.shadowRoot.querySelector('button').addEventListener('click', ...) - 如需统一处理多个内部按钮,可采用事件委托:
this.shadowRoot.addEventListener('click', e => { if (e.target.matches('[part="action"]')) { ... } }) - 切勿将逻辑与 DOM 结构强耦合——内部按钮更换标签或增加 wrapper 都会导致崩溃,建议使用
part属性配合::part()样式以及语义化事件分发机制
composed: false 的实际用途:防止事件“泄露”到不期望的位置
大多数场景下我们希望事件穿透边界,但某些内部行为不应暴露给外部——例如轮播组件自动切换时触发的 slide-change 事件,仅用于内部状态同步:
- 设置
composed: false后,该事件连宿主元素都无法收到,更不用说外部容器 - 这种事件只能在 shadow root 内部监听:
this.shadowRoot.addEventListener('slide-change', ...) - 注意:
composed: false不影响bubbles属性,事件仍然可以在 shadow 内部冒泡,只是不穿越边界 - 容易忽略的陷阱:调试时发现事件未触发,首先检查是否漏写了
composed: true,再确认监听位置是否位于 shadow 内部
事件穿透并非一个全局开关,而是逐层显式控制的过程。每个事件都需要单独决定是否设置 composed、在何处监听、以及如何获取源节点——既没有全局开关,也没有默认的“智能转发”机制。
