Shadow DOM事件默认不穿透,需显式设置composed: true才能跨边界;event.target始终为宿主元素,真实触发源须通过event.composedPath()[0]获取;手动派发事件必须声明composed: true,原生事件默认true但仅限浏览器触发。
composed: true 才能跨 Shadow BoundaryShadow DOM 内部触发的事件(比如点击一个 <button>)默认不会冒泡到宿主元素外侧——它被重定向后,event.target 显示为宿主元素(如 <my-button>),但原始触发节点藏在 event.composedPath()[0] 里。如果你希望事件能被外部监听到(比如父组件要响应子按钮点击),必须显式设置 composed: true:
new CustomEvent('click', { bubbles: true, composed: true }) —— 缺 composed: true,外部 addEventListener 永远收不到click、input)默认 composed 为 true,但仅限于浏览器原生触发;通过 dispatchEvent() 手动派发时,必须显式声明composed,若需兼容,得用 polyfill 或改用属性变更 + attributeChangedCallback 间接通信event.composedPath() 是唯一可靠方式获取真实触发源在宿主元素上监听事件时,event.target 总是返回宿主本身(如 <my-button>),不是内部按钮。想拿到真正被点的节点,只能靠 event.composedPath():
event.composedPath()[0] 是原始触发元素(如 shadow 内的 <span>)event.path —— 它是 Chrome 私有属性,Firefox/Safari 不一致,且已被标记为废弃event.composedPath().includes(this.shadowRoot)
event.target 做逻辑分支很多开发者习惯在自定义元素上写 this.addEventListener('click', e => { if (e.target.matches('button')) {...} }),这在 Shadow DOM 下会失效:
e.target 永远是 <my-button>,不可能匹配 'button'
this.shadowRoot.querySelector('button').addEventListener('click', ...)
this.shadowRoot.addEventListener('click', e => { if (e.target.matches('[part="action"]')) { ... } })
part 属性 + ::part() 样式 + 语义化事件分发composed: false 的实际用途:防止事件“泄露”到意外位置多数场景下你希望事件穿透,但有些内部行为不该暴露给外部——比如轮播组件自动切换时触发的 slide-change,仅用于内部状态同步:
composed: false 后,该事件连宿主元素都收不到,更别说外部容器this.shadowRoot.addEventListener('slide-change', ...)
composed: false 不影响 bubbles,它仍可在 shadow 内冒泡,只是不出边界composed: true,再确认监听位置是否在 shadow 内composed、在哪里监听、怎么取源节点——没有全局开关,也没有默认“智能转发”。