Shadow DOM仅提供样式和DOM查询隔离,不自动隔离JavaScript执行环境;逻辑隔离需开发者主动约束,如使用this.shadowRoot.querySelector、避免全局变量污染、显式设置事件composed属性等。
Shadow DOM 本身不提供“完全”隔离——它只隔离样式和 DOM 查询,逻辑(比如事件监听、属性响应、方法调用)仍需开发者主动约束。所谓“完全隔离”,其实是 Shadow DOM + Custom Elements + 显式边界设计 三者配合的结果。
调用 attachShadow() 只是挂载了一个影子根,但不会自动阻止外部 JS 访问内部节点或触发逻辑漏洞:
mode: 'open' 才能调试和测试;mode: 'closed' 会让 element.shadowRoot 返回 null,但无法真正防住有心人(比如通过 getEventListeners() 或重写原型),还导致 DevTools 无法 inspect,实际项目中基本不用attachShadow() 会抛出 Failed to execute 'attachShadow' on 'Element': Shadow root has already been attached 错误,必须在 constructor 中检查 this.shadowRoot 是否已存在connectedCallback 里反复调用 attachShadow(),否则每次插入都会报错Shadow DOM 默认阻断所有样式穿透,但你得主动暴露可控入口,否则组件就失去定制能力:
:host 只能选中宿主元素自身(即 <my-button> 这个标签),适合控制 display、margin、尺寸等“容器级”样式;加状态伪类如 :host([disabled]) 可响应属性变化::slotted(*) 匹配被 <slot> 接收的**顶层子元素**,不递归;例如 <my-card><h2 slot="title">Hi</h2><p>body</p></my-card> 中,::slotted(h2) 生效,但 ::slotted(p) 不会匹配到 <p>(它没带 slot 属性)/deep/ 或 ::shadow,现代替代是 ::part() + exportparts 属性,例如 <button part="icon"> 配合外部 my-button::part(icon) { color: red; }
Shadow DOM 内部触发的事件(如点击 <button>)默认会被重定向到宿主元素上,event.target 看起来是 <my-button>,但真实来源是内部按钮——这容易造成逻辑误判:
event.composedPath()[0] 才能拿到原始触发元素(需注意兼容性,IE 不支持)this.addEventListener('click', ...) 并假设 event.target 是自己期望的节点bubbles: true 和 composed: true 才能穿透 Shadow DOM 边界;缺 composed: true 就只能在 shadow 内部监听到真正的逻辑隔离不是 Shadow DOM 给的,是你写的每一行代码决定的:
this.shadowRoot.querySelector(),绝不能用 document.querySelector() —— 后者可能意外选中外部同名元素,尤其在微前端或多实例场景下innerHTML 直接拼接用户输入,防止 XSS;优先用 document.createElement() + textContent,或严格 sanitize 后再插入localStorage、sessionStorage 或全局变量,它们不属于组件边界;状态应封装在实例属性或通过 attributeChangedCallback 同步attributeChangedCallback 中修改样式时,只改 this.shadowRoot 下的节点,不要碰 document.head 或其他组件区域最常被忽略的一点:Shadow DOM 的“隔离”是单向 DOM 边界,不是沙箱。它不阻止 eval、不拦截 fetch、不限制 setTimeout,也不管你是否在内部调用了全局函数。所谓“完全隔离”,最终取决于你有没有把 this.shadowRoot 当成唯一可信的 DOM 根。