slot仅投影不移动dom节点,其内容仍属light dom树,parentnode不变,事件冒泡、查询、监听均遵循原始结构,shadowroot.children为空,::slotted()仅匹配直接投射的一层节点。

slot 不会移动 DOM 节点,只做“投影”
很多人以为写了 <slot></slot>,外部节点就被“挪进” Shadow DOM 里了。不是这样。<slot></slot> 只是声明一个投影点,浏览器在渲染时把 Light DOM 中匹配的节点“视觉上”显示在那里,但它们物理位置仍在 Light DOM 树中,parentNode、parentElement 都不会变。
这意味着:
- 事件冒泡路径仍按原始 Light DOM 结构走,不会穿过 Shadow Boundary
-
document.querySelector('h2')能查到,shadowRoot.querySelector('h2')查不到(除非它本来就在 shadow 内部) - 用
MutationObserver监听 Light DOM,能捕获到 slot 内容的增删;监听 shadowRoot 则捕获不到
DOM 树层级断裂:Light DOM 和 Shadow DOM 是两棵独立树
自定义元素调用 this.attachShadow({ mode: 'open' }) 后,它的内部就分裂出一棵新树。这棵树和外部 Light DOM 没有父子继承关系——shadowRoot.parentNode === null,即使它挂在 <my-card></my-card> 下面。
常见误判场景:
- 用
el.parentElement试图从 slot 内节点反查到自定义元素?不行,因为该节点的parentElement是<my-card></my-card>,不是 shadowRoot - 样式穿透失败?因为
:host > *只能选 shadow 内部直接子元素,而 slot 投影内容不属于 shadow 内部子元素 - 想用
shadowRoot.children看投进去的内容?它永远为空——投影内容不在 shadow 的 children 列表里
::slotted() 伪类只能选顶层投射节点
::slotted(*) 看似万能,其实限制极严:它只匹配被直接投射进来的**第一层元素节点**,不递归、不深入。
例如父级传入:<my-card><div><p>Hello</p></div></my-card>
-
::slotted(div)✅ 有效 -
::slotted(p)❌ 无效——<p></p>是<div> 的后代,不是 slot 直接接收的节点 <li> <code>::slotted(*)只命中<div>,不会命中其子节点 <p>如果真要样式穿透到深层,得靠 Light DOM 侧加 class,或在自定义元素内部手动遍历 <code>assignedNodes()并挂载 class。调试时容易忽略的“树不可见性”
浏览器开发者工具默认折叠 Shadow DOM,且 slot 投影内容在 Elements 面板里显示为灰色虚线(Chrome)或带 “#shadow-root” 提示(Firefox),但很多开发者没注意这点,直接在 shadowRoot 下搜索内容,自然找不到。
真正可靠的方式是:
- 在 Console 中执行
document.querySelector('my-card').shadowRoot.querySelector('slot').assignedNodes({ flatten: true }) - 看返回值是否包含预期节点——这是唯一能确认“内容是否真被投射进来”的方式
- 别依赖渲染结果判断:页面显示了 ≠ 内容已分配,可能只是 fallback 在撑场子
- 在 Console 中执行











