事件在 shadow dom 边界重定向是浏览器为保障封装性主动设计的行为:event.target 在外部监听中被替换为宿主元素,而 composedpath()[0] 始终返回原始触发节点,自定义事件需显式设置 composed: true 才能穿透边界。

事件在 Shadow DOM 边界发生重定向,不是 bug,而是浏览器为保障组件封装性主动做的设计。理解它,关键在于明白“谁触发了事件”和“谁对外可见”是两回事。
事件重定向的本质:目标被“替换成宿主”
当点击 Shadow 内部一个 <button></button>,事件冒泡到外部时,event.target 不再是那个 <button></button>,而是它的宿主元素(比如 <my-card></my-card>)。这是因为浏览器在穿越 Shadow Boundary 的过程中,自动把目标重定向为 Shadow Root 的宿主节点。
- 在 Shadow 内监听:target 是真实点击的按钮
- 在 document 或父组件监听:target 变成
<my-card></my-card>,哪怕你点的是里面第 3 个图标 - 这是为了防止外部脚本依赖内部结构——比如写
el.querySelector('.delete-btn').click(),一旦组件内部改类名就崩
怎么拿到真正被点击的元素?
别依赖 event.target,改用 event.composedPath()[0]:
-
composedPath()返回从原始触发点开始、沿冒泡路径的完整节点数组 - 索引 0 就是最初被点击/触发的那个元素,无论它在 Shadow 内还是 slot 投影进来
- 这个值在 Shadow 内外都可靠,且不被重定向影响
想让外部明确收到“内部发生了某事”,该怎么做?
原生事件(click、input、change)默认 composed: false,无法穿透边界。所谓“让事件出去”,其实是手动派发一个可穿透的自定义事件:
- 在 Shadow 内触发:
this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true })) - 外部直接监听:
host.addEventListener('submit', handler)或document.addEventListener('submit', handler) - 漏掉
composed: true是最常见错误——事件发了,但卡在 boundary 外面,外部完全收不到
slot 里的内容也受重定向影响吗?
是的。即使内容是通过 <slot></slot> 投影进来的(比如父组件传了个 <span class="label">OK</span>),只要它出现在 Shadow 内视觉区域,它的事件穿越边界时同样会被重定向为宿主元素。
- slot 节点物理上仍属于外部文档,但事件行为遵循 Shadow DOM 规则
- 所以
event.target在外部监听中仍是宿主,不是那个<span></span> - 仍可用
composedPath()[0]拿到它,前提是事件没被 stopPropagation










