前言
在大型项目开发中,最头疼的莫过于“样式污染”。你在 A 组件写了一个 .title,结果 B 组件的标题也变色了。Shadow DOM 正是为了解决这个问题而生的,它允许我们将隐藏的 DOM 树附加到常规的 DOM 树中,实现真正的样式隔离和组件封装。
一、 Shadow DOM 的核心概念
Shadow DOM 允许你在文档中创建一个“隔离区”。在这个区域内,所有的 CSS 样式和 DOM 结构都是私有的。
1. 四个关键词
-
Shadow Host(影子宿主) :影子 DOM 附加到的常规 DOM 节点(如示例中的
div)。 -
Shadow Root(影子根) :影子树的根节点。
-
Shadow Tree(影子树) :影子 DOM 内部的 DOM 树。
-
Mode(模式) :
open:可以通过 JavaScript 的host.shadowRoot访问内部。closed:禁止从外部访问影子根。
2. 基础用法
const host = document.createElement("div");
// 1. 创建影子根
const shadow = host.attachShadow({ mode: "open" });
// 2. 编写内部结构与样式
shadow.innerHTML = `
<style>
p { color: red; } /* 这里的样式不会影响外部的 p 标签 */
</style>
<p>我是被隔离的内容</p>
`;
document.body.appendChild(host);
二、 巧妙使用插槽 (Slot)
当你为一个元素附加 Shadow DOM 时,浏览器会优先渲染影子树,原本该元素内部的内容会被“吞掉”。为了保留并控制这些内容的显示位置,我们需要使用 <slot>。
1. 具名插槽 (Named Slots)
通过 name 属性,我们可以精确控制原内容渲染到影子树的哪个位置。
// 1. 原生 DOM 结构
document.body.innerHTML = `
<div id="parent">
<span slot="title">标题内容</span>
<span slot="desc">描述文字</span>
</div>`;
// 2. 附加 Shadow DOM 并使用插槽
document.querySelector("#parent").attachShadow({ mode: "open" }).innerHTML = `
<div class="card">
<h2><slot name="title"></slot></h2>
<p><slot name="desc"></slot></p>
</div>
`;
三、 影子 DOM 的事件冒泡
这是开发者最容易产生误解的地方。
1. 事件重定向
当 Shadow DOM 内部触发事件(如 click)并冒泡到外部时,为了维持封装性,浏览器会重定向事件源。
- 从外部看,事件的
target会被设置为 影子宿主(Shadow Host) ,而不是内部具体的按钮或文字。
2. 穿透边界
如果你确实需要知道是影子内部哪个元素触发的,可以使用 event.composedPath(),它会返回一个数组,包含事件流经的所有对象(包括影子内部节点)。
host.addEventListener('click', (e) => {
console.log(e.target); // 打印的是 host 元素
console.log(e.composedPath()); // 打印的是完整的事件路径,包含内部节点
});
四、 为什么需要 Shadow DOM?
- 样式隔离:内部样式不溢出,外部样式(除非继承属性)不进入。
- DOM 封装:外界无法通过
document.querySelector轻易获取组件内部细节。 - 代码复用:是构建跨框架组件(Web Components)的基石。
五、 面试模拟题
Q1:Shadow DOM 里的样式会继承外部样式吗?
参考回答: 大多数样式(如 background, width, border)是不会进入影子内部的。但具有继承性的 CSS 属性(如 color, font-family, line-height)依然会穿透影子边界,影响内部元素。
Q2:mode: "closed" 真的绝对安全吗?
参考回答: 不是绝对的。虽然无法通过 element.shadowRoot 访问,但开发者仍可以通过重写 Element.prototype.attachShadow 等 Hack 手段在根节点创建时拦截引用。在大多数生产场景下,建议使用 open 模式以方便调试。
Q3:Shadow DOM 与 Vue/React 的 Scoped CSS 有什么区别?
参考回答:
- Vue/React Scoped:是通过增加 Data 属性(如
[data-v-xxx])和 CSS 选择器实现的模拟隔离,DOM 依然在主文档树中。 - Shadow DOM:是浏览器原生层面的物理隔离,创建了独立的 DOM 树,隔离效果更彻底。