DOM
DOM (文档对象模型) 是 JS 操作网页的接口。面试中最常考的不是具体的 DOM API 怎么拼写,而是DOM 操作的性能瓶颈(重绘与回流)以及如何通过各种手段(如 DocumentFragment、虚拟 DOM)来减少对真实 DOM 的频繁操作。
节点操作核心 API
- 查找节点:
querySelector/querySelectorAll(返回静态 NodeList) vsgetElementsByClassName(返回动态 HTMLCollection,随 DOM 变化而实时更新)。 - 创建节点:
document.createElement、document.createTextNode。 - 修改节点:
appendChild、insertBefore、removeChild、replaceChild。 - 属性操作:
setAttribute、getAttribute。
重绘(Repaint)与回流/重排(Reflow)
这是前端性能优化的绝对核心,逢考必问!
- 回流 (Reflow):当 DOM 的变化影响了元素的几何信息(如宽度、高度、位置、隐藏/显示),浏览器需要重新计算 DOM 树中受影响节点的几何属性,并重新排列它们的位置。极其消耗性能。
- 触发条件:添加/删除可见 DOM、改变元素尺寸/位置、窗口 resize。
- 致命陷阱:当你通过 JS 获取某些布局属性时(如
offsetWidth、clientHeight、scrollTop、getComputedStyle),浏览器为了给你返回最精确的值,会强制清空渲染队列,立即触发一次回流。
- 重绘 (Repaint):当一个元素的外观发生改变,但没有改变布局,浏览器把新的外观绘制出来的过程。
- 触发条件:改变
color、background-color、visibility等。
- 触发条件:改变
- 关系:回流必将引起重绘,但重绘不一定会引起回流。
如何优化 DOM 操作性能?
面对高频 DOM 操作,我们可以采取以下策略来压榨性能:
- 合并样式修改:避免一行行修改
style,而是通过修改className或cssText一次性更新。 - 批量 DOM 操作 (离线操作):
- 使用
DocumentFragment(文档片段)。它存在于内存中,不在 DOM 树中。将成百上千个新节点插入到 fragment 中,最后一次性appendChild到页面上,只触发一次回流。 - 先将元素
display: none(触发一次回流),进行大量修改后,再display: block(触发第二次回流),避免了中间成百上千次回流。
- 使用
- 缓存布局信息:避免在循环中频繁读取
offsetWidth等会引发强制回流的属性,用一个变量把它缓存起来。 - CSS3 硬件加速:对于动画,尽量使用
transform和opacity。它们不会触发重绘和回流,而是直接交由 GPU 在合成线程(Compositor Thread)处理,性能极高。
虚拟 DOM 真的比原生 DOM 快吗?
面试官经常会抛出这个“陷阱题”来考察你的独立思考能力。很多初学者会被洗脑,认为“因为操作原生 DOM 慢,所以 Vue/React 的虚拟 DOM 快”。这是绝对错误的!
没有任何框架能比纯手工优化的原生 DOM 操作更快。
- 首次渲染:
- 原生:直接创建 DOM 并插入。
- 虚拟 DOM:先创建虚拟 DOM 对象 -> 通过框架转化为原生 DOM -> 插入。明显虚拟 DOM 更慢,因为它多了一层 JS 对象的计算。
- 微小局部更新:
- 原生:精确定位到那个节点,直接修改文本。
- 虚拟 DOM:重新生成整个组件的虚拟 DOM 树 -> Diff 算法对比新旧树找出差异 -> 修改真实的 DOM。这里 Diff 算法的开销是不容忽视的。
- 复杂列表更新(比如打乱顺序):
- 原生:如果没有深厚的功底,很容易写出全部卸载再重新挂载的灾难级代码(引发剧烈回流)。
- 虚拟 DOM:通过 Diff 算法和 Key 机制,计算出最小的移动/复用路径,最后统一打补丁(Patch)。
总结: “虚拟 DOM 并不是为了追求极致的性能而诞生的。它的真正价值在于‘开发体验’与‘性能’之间的绝佳平衡。 它为我们提供了一种声明式的 UI 编程方式,让我们不用再手动去追踪状态和精细化操作 DOM;同时,它在底层通过 Diff 算法和批量更新(Batching)机制,保证了即使在极其复杂的交互下,我们的代码依然能保持在性能的及格线(下限)之上。此外,虚拟 DOM 的抽象层也为跨端(如 React Native、Weex)提供了可能。”