Shadow DOM中自定义事件target混乱的根本原因是未设composed:true;必须同时满足bubbles:true和composed:true,外部才能正确接收事件,且event.target被重定向为宿主元素属正常设计,真实目标需用event.composedPath()[0]获取。

Shadow DOM 中自定义事件的 target 指向混乱,根本原因不是代码写错了,而是对 composed 属性的理解偏差或遗漏。只要确保自定义事件满足两个条件——bubbles: true 且 composed: true——外部就能正确收到,并拿到符合预期的 target。
为什么自定义事件的 target 还是宿主元素?
即使你派发了自定义事件,如果漏掉 composed: true,事件在穿过 Shadow Boundary 时仍会触发目标重定向(retargeting),event.target 就会被浏览器自动改成宿主元素(如 <my-input>),而非你期望的内部按钮或输入框。
这个行为和原生 click 一样,是 Shadow DOM 的封装机制,不是 bug。关键区别在于:原生事件无法开启 composed,而自定义事件可以——但必须显式声明。
-
composed: false(默认)→ 事件止步于 shadow boundary,外部监听器收不到 -
composed: true→ 事件可穿透边界,外部能收到,且event.target在外部表现为宿主元素(这是正常设计) - 若需在外部获取真实触发元素,请用
event.composedPath()[0],它始终返回最深的原始目标
如何让外部准确识别点击/触发源?
依赖 event.target 判断内部结构,在 Shadow DOM 场景下天然不可靠。应统一改用事件路径(composed path)来还原真实上下文。
- 在 Shadow 内部派发事件时,附带业务标识:
new CustomEvent('submit', { bubbles: true, composed: true, detail: { type: 'save', formId: 'user-profile' } }) - 外部监听时,不再写
if (e.target.classList.contains('save-btn')),而是用e.composedPath()[0]定位真实元素 - 例如:
const realBtn = e.composedPath()[0]; if (realBtn.matches('[data-action="delete"]')) { ... }
兼容旧版 React(≤16)的特别处理
React 16 及更早版本把所有事件代理到 document,靠 event.target 分发合成事件。一旦被重定向,onClick 就失效。此时不能依赖宿主元素上的 onclick 属性绑定。
- 避免在宿主标签上直接写
<my-button onclick="handle()> - 改用自定义事件通信:组件内部派发
ui-click,父组件用useEffect + addEventListener监听 - 若必须用 React 合成事件,建议升级至 React 17+,它已原生支持
composed: true事件的正确分发
slot 投影内容的事件怎么处理?
通过 <slot> 投入的节点(比如外部传入的 <button slot="action">),其事件依然受 Shadow Boundary 约束。视觉上它“在组件里”,逻辑上它的事件目标仍会被重定向为宿主元素。
- 不要在外部用
querySelector去找 slot 里的元素并绑事件——它不在 light DOM 的常规查询路径中 - 推荐做法:在组件内部的
slotchange回调中,遍历event.target.assignedNodes(),对投影节点手动添加监听,并派发composed: true的自定义事件 - 这样既保持封装,又让外部能响应 slot 内容的真实交互

















