第一次听到 Shadow DOM 和 Virtual DOM 的时候,我也懵了。
两个都叫 DOM,长得还挺像,是不是有什么关系?是不是一个是另一个的升级版?
答案是:它们除了名字里都有 "DOM",几乎没有任何关系。
一个是浏览器原生能力,一个是框架层面的性能优化手段。今天咱们就把这俩彻底掰扯清楚,以后面试被问到也能对答如流。
一、先看个段子热热身
面试官:说说你对 Shadow DOM 和 Virtual DOM 的理解。
我:Shadow DOM 是影子,Virtual DOM 是虚拟的,加起来就是"虚拟的影子"。
面试官:???你可以回去了。
玩笑归玩笑,下面进入正题。
二、Virtual DOM(虚拟 DOM)
2.1 它是什么
Virtual DOM 本质上就是用 JavaScript 对象来描述真实 DOM 结构。
比如这段 HTML:
html
xml
<div id="app" class="container">
<h1>Hello</h1>
<p>World</p>
</div>
用 Virtual DOM 表示大概是这样的:
js
css
{
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', children: 'Hello' },
{ tag: 'p', children: 'World' }
]
}
它就是一个普普通通的 JS 对象,存在内存里,跟浏览器没半毛钱关系。
2.2 为什么需要它
直接操作真实 DOM 很慢吗?其实单次操作并不慢 ,慢的是频繁操作引发的重排(reflow)和重绘(repaint) 。
早期 jQuery 时代,我们这样写:
js
javascript
$('#app').html('<h1>Hello</h1>');
$('#app').append('<p>World</p>');
$('#list').empty();
每次操作都直接怼到真实 DOM 上,浏览器就得重新计算布局。数据一变就来一轮,性能直接裂开。
Virtual DOM 的思路是:
- 数据变化时,先在内存里生成一棵新的 Virtual DOM 树
- 和旧的树做 diff 对比,找出真正变化的部分
- 只把这些变化批量、最小化地更新到真实 DOM
用一句话概括:用 JS 的计算成本,换 DOM 的更新成本。
2.3 它解决了什么
- ✅ 减少不必要的 DOM 操作
- ✅ 让开发者可以"声明式"写 UI,不用手动操作 DOM
- ✅ 跨平台能力(React Native、小程序等都受益于此)
2.4 它不解决什么
- ❌ 它不解决样式隔离问题
- ❌ 它不解决DOM 结构封装问题
- ❌ 它只是框架内部的优化手段,不是浏览器标准
三、Shadow DOM(影子 DOM)
3.1 它是什么
Shadow DOM 是 Web Components 标准 的一部分,是浏览器原生支持的能力。
它的核心作用是:把一棵 DOM 子树封装起来,让它和外部的 DOM 互相隔离。
听起来有点抽象,上代码:
html
xml
<div id="host"></div>
<script>
const host = document.getElementById('host');
const shadowRoot = host.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
p { color: red; font-size: 20px; }
</style>
<p>我是影子 DOM 里的段落</p>
`;
</script>
外部写:
css
css
p { color: blue; }
影子 DOM 里的 p 依然是红色,外部样式进不去,内部样式也出不来。
这就是样式隔离 + DOM 隔离。
3.2 它的关键特性
| 特性 | 说明 |
|---|---|
| 样式隔离 | 外部 CSS 选择器无法穿透影子边界 |
| DOM 封装 | document.querySelector 找不到影子内部节点 |
| 事件重定向 | 内部事件冒泡到宿主元素时,target 会被重定向 |
| 原生支持 | 不需要任何框架,浏览器自带 |
3.3 它解决了什么
- ✅ 组件样式污染问题(想想那些
.btn冲突的血案) - ✅ 组件的真正封装(内部结构外部拿不到)
- ✅ 微前端场景下的样式隔离
- ✅
<video>、<input>这些原生控件内部其实就用了 Shadow DOM
3.4 它不解决什么
- ❌ 不解决性能问题
- ❌ 不解决数据驱动视图的问题
- ❌ 兼容性相对复杂(虽然现在主流浏览器都支持了)
四、核心对比:一张表说清楚
| 维度 | Virtual DOM | Shadow DOM |
|---|---|---|
| 本质 | JS 对象树 | 真实的 DOM 子树 |
| 归属 | 框架实现(React/Vue) | 浏览器原生标准 |
| 目的 | 性能优化 + 声明式 UI | 封装 + 样式隔离 |
| 是否真实存在 | 否,只在内存里 | 是,真实 DOM |
| 能否操作样式 | 不涉及 | 核心能力 |
| 能否提高性能 | 核心目标 | 不涉及 |
| 跨平台 | 是 | 否,仅浏览器 |
| 出现时间 | 2010 年左右(React 带火) | 2011 年左右(Web Components) |
一句话总结:
Virtual DOM 解决的是"怎么更新得更快 ",Shadow DOM 解决的是"怎么封装得更好"。它俩根本不在一个赛道上。
五、常见的三个误区
误区一:Shadow DOM 是 Virtual DOM 的升级版 ❌
完全不是。一个是框架层,一个是浏览器层,没有谁替代谁的关系。
误区二:用了 Shadow DOM 性能会更好 ❌
Shadow DOM 主要解决封装问题,性能提升不是它的目标。某些场景下反而因为有边界的存在,样式计算会更慢一点。
误区三:Virtual DOM 比直接操作 DOM 快 ❌
这是被传烂了的说法。Virtual DOM 本身有 diff 开销,它不一定比手动优化过的 DOM 操作快 。它的价值在于:让你在不用手动优化的前提下,也能获得不错的性能。
尤雨溪本人也说过类似的话:Virtual DOM 的性价比在于"可维护性 + 性能"的平衡。
六、它们能一起用吗?
当然可以,而且很常见。
比如你用 Vue 或 React 开发,同时引入了一个基于 Web Components 的 UI 库(比如某些设计系统组件),这时候:
- 框架内部用 Virtual DOM 做 diff 更新
- 自定义元素内部用 Shadow DOM 做样式隔离
两者井水不犯河水,各干各的活。
举个 React 的例子:
jsx
javascript
function App() {
return (
<div>
<h1>React 渲染的内容</h1>
<my-web-component /> {/* 内部使用 Shadow DOM */}
</div>
);
}
React 负责 Virtual DOM 那层,my-web-component 内部自己玩 Shadow DOM。
七、总结
再用一张图帮大家记忆:
text
markdown
┌─────────────────────────────────────┐
│ 浏览器真实 DOM │
│ ┌──────────────┐ ┌─────────────┐ │
│ │ 普通 DOM 树 │ │ Shadow DOM │ │
│ │ │ │ (封装) │ │
│ └──────────────┘ └─────────────┘ │
└─────────────────────────────────────┘
▲
│ 批量更新
│
┌─────────────────────────────────────┐
│ Virtual DOM(内存中的 JS 对象) │
│ React / Vue 在维护 │
└─────────────────────────────────────┘
记住两句话就够了:
- Virtual DOM 是"内存里的 DOM 草稿",用来算最小更新量。
- Shadow DOM 是"DOM 里的保险箱",用来做样式和结构的封装。
下次再有人问你这两个的区别,直接把这篇文章甩过去 😎
如果这篇文章对你有帮助,点个赞再走呗~ 有问题欢迎评论区交流!