当你知道的越多,就越发现 DOM 这个看起来简单的东西,其实是整个前端世界的地基。很多人在初学阶段把 DOM 当作“用 JS 改网页内容的工具”,等面试被问到“虚拟 DOM 和 diff 算法”“事件冒泡、事件捕获、事件委托”时,才发现自己连 DOM 的边界都没划清。这篇文章我准备一次性讲透,把 DOM 从是什么、怎么构建,到怎么渲染、怎么操作,再到虚拟 DOM 和事件机制这些高频考点全部串起来,适合刚入门的前端,也适合准备面试但想系统复盘一遍的同学。
先交代一句:DOM 全称叫 Document Object Model,中文是“文档对象模型”。它不是一门语言,也不是浏览器的某个具体功能模块,而是浏览器暴露给脚本的一棵可操作的内存结构树。你写的 HTML 只是文本,真正让 JavaScript 能“看见”页面元素并修改它们,靠的全是这棵 DOM 树。
1. 拆开缩写的三个词:文档、对象、模型,凭什么组成一门技术
1.1 文档、对象、模型三个词到底是什么意思
很多人只记住了“DOM 是操作页面的接口”,但这三个词其实精准描述了它存在的方式。
“文档”指的是 HTML 文档本身,也就是你在编辑器里写的那一串标签文本。浏览器拿到这串文本之后,会把它解析成结构化的数据。注意这里的“解析”很关键,因为文本本身没有任何结构化能力,<div>和<span>谁是父子关系,必须要通过语法分析才能确定。
“对象”指的是内存中的一个个节点实例。每个 HTML 标签、文本、注释、属性,在 DOM 里都是一个“对象”,这些对象有自己的属性和方法。比如一个<div>标签,在 DOM 中是HTMLDivElement的实例,它继承了HTMLElement、Element、Node这些原型链上的能力,所以你才能调用appendChild()、querySelector()这类方法。
“模型”指的是整个文档的结构被抽象成了一种“树模型”,也就是节点之间的关系。树的根是document,往下依次是html、head、body,再往下一层层展开。DOM 提供了一整套对这个树模型进行读取、插入、删除、替换的 API,浏览器会根据这棵树的实时状态决定要不要重新渲染。
一句话总结:DOM 是 HTML 文档在内存里的一种树形对象表示,它既是数据的结构,也是操作的入口。没有 DOM,JavaScript 就是一门和网页毫无关系的语言。
1.2 浏览器是怎么把 HTML 变成 DOM 树的
这个过程值得仔细讲,因为它决定了你对“网页渲染”的很多判断。
浏览器拿到 HTML 字节流之后,第一步是字节解码,把网络传输过来的字节按编码规则转成字符,比如 UTF-8 解码成可读的文本。第二步是词法分析,也叫 tokenization,把字符串拆成一个一个的 token,比如开始标签<div>、属性class="box"、文本内容、结束标签</div>,这些 token 都有类型,标记着它们在文档中的角色。第三步是语法分析,也就是构建节点树,浏览器按照 HTML 规范里定义的规则,把 token 组装成嵌套的节点。
这里有个很有意思的细节:HTML 解析并不是“洗干净之后一次性建树”,而是一个边读边建的过程。浏览器维护着一个“解析器状态机”,读到开始标签就创建一个元素节点并压入栈,读到文本就追加到当前节点的文本内容里,读到结束标签就出栈。碰到标签嵌套错误,比如<p><div></p></div>,解析器不会直接报错,而是按照规范里的“错误处理规则”去修正,自动补全或者重组结构。
原因在于 HTML 是一个“宽容”的语言。浏览器厂商设计解析器时的共识是:网页不能被语法错误彻底卡死,能修就修,保证用户永远都能看到内容。所以你在浏览器里看到的 DOM 结构,未必和你编辑器里写的源码一字不差,它已经是“修正优化”过的一版。
1.3 DOM 和 HTML、CSSOM 的关系别再混为一谈
初学者最容易搞混的一件事,是把“HTML 文本”直接当成“DOM”。其实二者是源数据与结构化表达的关系。HTML 是静态的,刷新之后它会重新解析一遍;DOM 是动态的,JS 改了它,页面上的内容立刻变化,但这个变化并不会回写到 HTML 源文件里,刷新页面之后又恢复原样。
还需要分清 DOM 不是仅有的“对象模型”,浏览器里还有一个和它并列的结构叫 CSSOM,也就是 CSS 对象模型。CSSOM 是浏览器解析 CSS 文本生成的样式规则树,它负责回答“这个元素最终该长什么样”。渲染时,浏览器会把 DOM 和 CSSOM 合并成渲染树,只有渲染树上能够影响视觉布局的节点才会被真正绘制。display: none的节点会从渲染树里被剔除,但它依然在 DOM 树里。这就是为什么getElementById依然能找到那个元素,只是因为样式隐藏所以看不见而已。
我把 DOM、CSSOM、渲染树三者的关系用一张表对给你:
| 结构 | 来源 | 职责 | 是否直接决定视觉 |
|---|---|---|---|
| DOM | HTML 解析 | 描述页面结构和内容 | 间接 |
| CSSOM | CSS 解析 | 描述样式规则和层叠关系 | 间接 |
| Render Tree | DOM 与 CSSOM 合并 | 提供布局与绘制的节点集合 | 直接 |
理解了它们之间的差别,后面的渲染性能问题就很好解释了。
2. 浏览器渲染流程与“从上到下”顺序渲染的真相
2.1 从 HTML 到界面,浏览器到底做了几次工
渲染流程可以概括为五个大阶段:解析、样式计算、布局、绘制、合成。
解析阶段生成了 DOM 和 CSSOM,这一点前面已经说过。样式计算阶段,浏览器会根据 CSSOM 的规则,为 DOM 树里的每个可见节点计算出最终的样式值,这个阶段会处理继承、层叠、单位换算,比如把em、vh等相对单位换算成像素。布局阶段,也叫 reflow,浏览器会计算每个元素在页面上的几何位置和尺寸,包括它相对于父容器和兄弟节点的偏移量。绘制阶段,把每个节点的背景、边框、文字、阴影等视觉属性绘制到多个图层上。合成阶段,浏览器把各个图层按正确的层级顺序交给 GPU 合成,最终显示在屏幕上。
这里有一个重要判断:上面说的是“一次完整渲染”的流程,但真实页面的首次加载往往不是一次性的。HTML 是边解析边构建 DOM 的,只要解析器先读完了<head>里的内容,并且 CSS 也没有阻塞,浏览器就可能先绘制出第一屏的内容,然后继续解析主体部分。这种机制叫做渐进式渲染,是浏览器特意为了“最早时间看到内容”而设计的。
2.2 “DOM 从上到下顺序渲染是不是更快”?答案没那么简单
很多开发者问:DOM 从上到下按顺序渲染,是不是就更快?这个说法有一定道理,但远没有“顺序决定一切”那么绝对。
说它有道理,是因为浏览器的 HTML 解析器确实是按照字节流的顺序,从上到下扫描、解析、构建节点的。按理说,HTML 顶部的内容总是会先被解析到,理论上也会先出现在渲染结果里。但真实情况受两个关键因素影响:CSS 和 JavaScript。
CSS 会阻塞渲染。浏览器只有在解析完样式表之后才能进行样式计算,所以你如果把<style>或者外部样式表放在页面底部,浏览器会先解析出一部分 DOM,却发现没有样式规则可以做计算,最终在布局之前停下来等待。等到 CSS 到了,它再重新计算。这反而拖慢了首屏速度。因此业界惯例是把 CSS 放在<head>里,让样式尽早就位。
JavaScript 会阻塞解析。当解析器碰到<script>标签时,按照早期 HTML 规范,它必须停下来等脚本执行完,因为脚本里可能调用了document.write()来修改文档流。阻塞期间,DOM 构建暂停,渲染也就停住。为此,我们才把普通脚本放在</body>前,或者给脚本加defer和async。
回到“从上到下顺序渲染是不是更快”这个问题,比较准确的回答是:从上到下顺序解析确实让渐进式渲染成为可能,看起来是“边读边画”,但实际速度取决于有多少渲染阻塞资源。顺序本身不是性能瓶颈,瓶颈在于你的 CSS 和脚本放置策略是否让解析器“空转等待”。
2.3 回流与重绘:为什么操作 DOM 慢,就慢在这里
前端圈子里有一句老话:操作 DOM 是最贵的操作。贵就贵在当你修改 DOM 时,浏览器可能被迫重新执行“布局”甚至“绘制”。
回流(Reflow)指的是布局阶段被重新触发。当你改变元素的尺寸、位置、内容、字体大小,或者增删节点时,浏览器需要重新计算受影响区域里所有元素的几何属性。最糟糕的情况是修改了根节点附近的结构,整个页面都重新布局。比如改body的宽度,所有子元素的排列都要重新算一遍。
重绘(Repaint)指的是绘制阶段被重新触发。当你只修改颜色、背景、阴影这些不影响布局的属性时,浏览器不需要重新算几何,只需要把像素重新画一遍。重绘的代价虽然比回流小,但也不是免费的,因为浏览器还是要遍历图层、计算绘制指令。
在实际开发里,常见的“慢”是回流和重绘交叠出现。比如你用一个循环去逐个插入列表项,每插入一项都可能触发一次回流,几十上百次循环下来性能直接崩。解决思路很清晰:把 DOM 操作合并,减少触发次数,我推荐以下几种做法:
- 批量读取再批量写入,不要在读写之间交替触发布局。
- 使用
DocumentFragment先构建子节点,再一次性插入。 - 用
display: none先隐藏元素,操作完再显示,只触发两次回流,而不是 N 次。 - 动画里优先用
transform和opacity,因为它们能走合成层,不触发布局和绘制。
2.4 一套可以直接落地的渲染性能优化清单
这部分是我在实际项目中整理出来的,一般项目照着做都不会差:
- 把 CSS 放在
<head>,保证样式尽早参与渲染。 - 把 JS 放在
</body>前,或使用defer/async避免阻塞。 - 减少 DOM 嵌套层级,特别是深层无序的嵌套,会拖慢样式计算速度。
- 用
classList切换多个样式类,不要一条一条地改style属性。 - 避免频繁读取
offsetWidth、offsetHeight、getComputedStyle等布局属性,它们在读取时会强制刷新布局队列。 - 利用事件委托,减少事件监听器的数量。
- 数据列表用虚拟滚动,只渲染可见区域,控制 DOM 节点总量。
这些优化背后其实都是同一个原则:让浏览器少干活。DOM 操作本身不慢,慢的是你可能不小心让浏览器重复干了很多不必要的活。
3. 日常开发里高频用到的 DOM 操作技法
3.1 一张表看懂查、增、改、删四类操作
DOM 提供的 API 非常多,但日常开发真正高频用到的不外乎“查、增、改、删”四类。我把它们整理成表,顺手标注了每个方法的特点和使用场景。
| 类型 | 常用 API | 说明 | 注意点 |
|---|---|---|---|
| 查询 | document.getElementById() | 按 id 找单个元素 | 老牌 API,性能极佳 |
| 查询 | document.querySelector() | 按 CSS 选择器找单个元素 | 方便但比 getElementById 慢 |
| 查询 | document.querySelectorAll() | 按选择器找一组元素 | 返回静态 NodeList |
| 查询 | element.closest() | 从自身向上找符合选择器的祖先 | 事件委托里的神器 |
| 新增 | document.createElement() | 创建元素节点 | 需配合 append 使用 |
| 新增 | element.insertAdjacentHTML() | 在指定位置插入 HTML | 慎用,涉及 XSS 风险 |
| 新增 | element.append()/prepend() | 尾部/头部添加子节点 | 支持多节点和字符串 |
| 修改 | element.textContent | 设置或读取文本内容 | 设置时自动转义标签 |
| 修改 | element.setAttribute() | 设置属性 | 可直接赋值部分属性 |
| 修改 | element.classList | 操作 class | 比操作 className 更方便 |
| 删除 | element.remove() | 直接移除元素 | 兼容性没问题 |
| 删除 | element.replaceWith() | 把当前元素替换成另一个节点 | 一步到位,避免手动删再插 |
一个值得强调的点是textContent和innerHTML的区别。textContent会把内容当作纯文本插入,标签会被自动转义成普通文本;innerHTML则会把字符串当作 HTML 解析。前者安全,后者在数据可控的情况下才建议用。
3.2 批量插入的正确姿势:DocumentFragment 与一次性重绘
如果你需要在页面里添加一个包含 100 个列表项的<ul>,最糟糕的写法是循环 100 次,每次都创建节点并appendChild,这样触发的回流次数可能上百次。正确的姿势是利用DocumentFragment做一个“离线容器”。
const ul = document.querySelector('#list'); const fragment = document.createDocumentFragment(); for (let i = 0; i < 100; i++) { const li = document.createElement('li'); li.textContent = `第 ${i + 1} 项`; fragment.appendChild(li); } ul.appendChild(fragment);DocumentFragment是一种特殊的节点,它可以临时存储子节点,但不会成为真实 DOM 的一部分,因此你对它的所有修改都不会触发回流。当你把它一次性塞进ul时,浏览器只执行一次插入操作,DOM 更新也就只触发一次回流。实测下来,这种写法在渲染上百条数据时性能提升非常明显。
这里要住的细节是:fragment.appendChild(li)之后,li会暂时从 fragment 里“脱离”,但当你把fragment插入ul时,fragment里的所有子节点会自动转移到ul下,fragment自己则变回空状态。这是大部分前端都不太注意,面试里偶尔会被问到的一个小考点。
3.3 动画场景里绕开“布局抖动”的实战技巧
我自己在写复杂动画时踩过好几次坑,最典型的是“布局抖动”引发的掉帧。布局抖动指的是:你在同一个函数里既读取了布局属性,又修改了 DOM 属性,导致浏览器被迫反复执行同步布局。
举个例子:
// 问题代码:读写交替,触发多次回流 for (let i = 0; i < items.length; i++) { const width = items[i].offsetWidth; // 读取 items[i].style.width = width + 10 + 'px'; // 写入 }正确做法是把读取和写入分成两个阶段。先读所有需要的布局数据,存到数组里,再统一写入。
// 优化代码:先读后写 const widths = []; for (let i = 0; i < items.length; i++) { widths.push(items[i].offsetWidth); } for (let i = 0; i < items.length; i++) { items[i].style.width = widths[i] + 10 + 'px'; }动画循环里也是一样的道理。如果你在requestAnimationFrame回调里既读取又写入,浏览器没办法把多个帧的布局工作合并起来,动画就卡顿。更推荐的做法是:用transform来实现位移和缩放动画,因为 transform 只触发合成层的更新,走 GPU 加速,完全绕开布局和绘制阶段。
4. 事件模型:冒泡、捕获、委托一网打尽
4.1 DOM 事件流的三个阶段,别再只背一个“捕获”
几乎每个前端面试都会问到事件流。完整的事件流分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
你点击页面上一个按钮时,事件的完整传播路径是这样的:从window出发,经过document,再到html,然后是body,一路向下到目标元素的外层容器,最终到达目标元素本身。这个“从根到目标”的下行过程就是捕获阶段。命中目标元素的那一刻,事件处于目标阶段,事件监听器开始在目标上执行。紧接着,事件沿原路返回——从目标元素一层层向上传播——这个过程就是冒泡阶段。
给你看一个直观的例子。假设结构是body > div > button,你点了button,事件传播顺序是:
window -> document -> html -> body -> div -> button(目标阶段) -> div -> body -> html -> document -> window
addEventListener的第三个参数控制监听器在哪个阶段触发。默认是false,表示监听器在冒泡阶段触发;传true表示在捕获阶段触发。
const div = document.querySelector('div'); // 冒泡阶段触发 div.addEventListener('click', () => { console.log('冒泡阶段'); }, false); // 捕获阶段触发 div.addEventListener('click', () => { console.log('捕获阶段'); }, true);面试的时候,能够完整说出这两个阶段的区别,并且清楚第三个参数的默认值,就比大部分只会说“事件冒泡”的候选人强很多。
4.2 冒泡与捕获的实际差异:从代码出发理解
写页面的时候,为什么我们常常对“冒泡”讨论得更多?因为实际开发里绝大多数监听器都挂在冒泡阶段,它的行为更符合直觉:事件从目标往上走,父容器能统一收到所有子元素的事件。
捕获阶段用得少,但它在某些场景下很关键。比如你想在所有子元素之前拦截某个事件,就应该在捕获阶段监听。一个常见需求:页面里有一个列表,你希望点击任何区域的空白处都收起菜单,但不希望点击菜单本身时也触发收起。你可以在document的捕获阶段判断目标是不是菜单内部的元素,提前决定要不要阻止它。
document.addEventListener('click', (e) => { if (!menu.contains(e.target)) { menu.hide(); } }, true);这里第二个参数true必不可少。因为你如果写在冒泡阶段,事件里那些子元素上的监听器可能已经把数据修改掉了,或者已经执行了某些业务逻辑。捕获阶段监听能让你的“兜底逻辑”最早介入。
顺带提一下两个容易混的方法:
stopPropagation():阻止事件继续传播,冒泡阶段被调用后,父元素不会再收到该事件。stopImmediatePropagation():不仅阻止传播,还阻止当前元素上其他监听器的执行。preventDefault():阻止浏览器默认行为,比如阻止跳转链接,但它不会阻止事件传播。
三者的边界一定要分清楚,面试时会被追问。
4.3 事件委托:最经典也最常被问到的性能技巧
事件委托的核心思路其实很简单:因为事件有冒泡机制,子元素的点击事件会冒泡到父元素,所以我们可以只在父元素上绑定一个监听器,通过判断event.target来处理任意一个子元素的事件。
经典场景是动态列表:列表项由接口返回,用户也能随时新增项,如果给每一项都绑定监听器,新增时还得重新绑定,非常麻烦。事件委托只需要在容器上绑定一次:
document.querySelector('#list').addEventListener('click', (e) => { const item = e.target.closest('.list-item'); if (!item) return; item.classList.toggle('selected'); });这段代码里有几个关键点。第一,e.target是真正被点击的元素,可能是.list-item里的某个子元素,所以要用closest()从它自己向上找到.list-item这个祖先。第二,closest()找不到时返回null,所以要提前return掉。第三,事件委托天然覆盖“未来新增的节点”,这是它最值钱的地方。
面试时如果被问到“事件委托的优点”,你可以这样答:减少内存占用,因为监听器数量少;提高绑定效率,一次性绑定代替大量循环;支持动态节点,新增元素不需要重复绑定。如果被追问“缺点”,也值得说清楚,事件委托依赖事件冒泡,focus、blur这类不冒泡的事件没法直接委托,但可以用focusin、focusout代替。
5. 虚拟 DOM 与 diff 算法:面试高频考点拆解
5.1 虚拟 DOM 到底是虚拟了什么
很多面试者把虚拟 DOM 背成“用 JS 对象模拟 DOM”,但为什么需要这样一个模拟结构,真的理解的人不多。
核心原因是:直接操作真实 DOM 代价高。真实 DOM 节点包含大量的属性,一个普通的<div>节点上可能有几十上百个内置属性和方法,频繁创建、比较、更新它们是很重的。而虚拟 DOM 用一个极简的 JS 对象描述节点信息,它只记录标签名、属性、子节点等关键数据,相比真实节点轻得多。
// 一个最简单的虚拟 DOM 节点结构 const vnode = { tag: 'div', props: { id: 'app', class: 'container' }, children: [ { tag: 'h1', props: {}, children: ['标题'] } ] };渲染流程是这样的:组件运行 render 函数,生成新的虚拟 DOM 树;框架把新树和旧树作 diff 比较,计算出差异集合;框架只把差异同步到真实 DOM 上,而不是直接重建整棵树。这样一来,即使页面发生大量状态变化,真实 DOM 的操作次数也被压到了最小。
虚拟 DOM 最大的价值其实不是“比手写 DOM 操作更快”,而是它把“描述页面状态”和“更新真实 DOM”这两个过程解耦了。开发者只需要描述页面“应该长什么样”,至于怎么高效地更新,交给框架处理。这种抽象让跨平台成为可能,因为你可以在不同平台上用同一套描述逻辑,渲染到 DOM、Canvas、甚至原生组件。
5.2 Diff 算法核心:用“最小差异”换可接受性能
diff 算法要解决的问题是:给定两棵虚拟 DOM 树,如何找到它们之间最小的更新差异?
理论上,两棵树的最小差异比较是一个非常复杂的算法,时间复杂度会高到不可接受。所以 Vue 和 React 都采取了一个“牺牲理论最优、换取实际可用”的策略:只在同一层级做比较,不跨层级移动节点。这基于一个实践中几乎总是成立的假设:DOM 节点不会无端地从一处被移到另一处,跨层级的操作是极少数。
在同层比较时,框架会逐个对比新旧节点的tag和props。如果节点类型相同,就继续深入地比较子节点;如果节点类型不同,直接把旧节点整个替换掉,不再花时间比较它的子树。这种“同层比较 + 类型不同即替换”的规则,把 diff 的时间复杂度压到了接近 O(n)。
为了让重复节点能被复用,框架引入了key。key是节点在列表中的唯一标识,当新旧列表结构相似但顺序发生变化时,diff 算法可以通过key判断出哪些节点是“同一个”,然后执行移动而不是删除重建。这就是为什么在 Vue 的v-for列表里,官方一直要求你绑定一个稳定唯一的key,而不要用数组索引。
5.3 React 和 Vue 的 diff 到底差在哪
这个问题经常在面试中出现。从宏观上讲,两者的思路一致,都是同层比较 + key 优化,但微观细节有明显差异。
Vue 2 的 diff 采用的是“双端比较”策略。它会同时从新旧两个数组的两端开始,尝试用头部、尾部节点做比较,优先处理“头头相同、尾尾相同、头尾相同”这几种常见情况,把能复用的节点尽量找出来。这种策略在列表只是末尾新增项、头部新增项等场景里有明显优势,能够用最少的移动次数完成更新。
React 从较新的版本开始,diff 的实现往“Fiber 架构”演化。它允许 diff 过程被拆分成多个小任务,按优先级分片执行,而不是一次性同步完成,这样浏览器可以在 diff 过程中穿插处理用户输入和动画,不至于因为长时间占用主线程而卡顿。
如果你在面试时能说出这种层次差异,就能给面试官留下“真读过源码级内容”的印象。但要记住,面试官最关心的不是你能背多少细节,而是你有没有理解虚拟 DOM 解决的问题:通过最小化真实 DOM 操作,让开发者的心智负担和框架的性能表现达成平衡。
6. 真实的 DOM 世界:安全、歧义与面试清单
6.1 DOM 型 XSS:不是网上随便说说那么简单
“DOM 型 XSS”是 Web 安全领域的一个老话题,和 DOM 有直接关系。它的原理是:前端代码将不可信的外部数据直接插入到 DOM 中,导致恶意脚本在页面上下文里被解析和执行。
最常见的场景是使用innerHTML:
const name = new URLSearchParams(window.location.search).get('name'); document.querySelector('#welcome').innerHTML = `欢迎回来,${name}`;如果攻击者构造一个这样的链接:
https://example.com/?name=<img src=x onerror=alert(document.cookie)>那么innerHTML会把这个字符串当作 HTML 解析,img标签被创建,onerror里的脚本自动执行,alert(document.cookie)弹出。这只是一个演示,真正的攻击者可以窃取 Cookie、修改页面内容、伪装成登录框骗取密码。
防御的核心原则是:不要用 HTML 解析器处理不可信数据。登一条入textContent或innerText代替innerHTML,数据就会被当作纯文本插入,标签失去效果。此外,服务端对输入做严格的校验和编码,响应头配置正确的 Content-Security-Policy,都能有效降低 DOM 型 XSS 的风险。
面试时被问到 XSS,能结合 DOM 操作讲清楚攻击链路,并且主动说出textContent和innerHTML的本质区别,就已经能体现你对 Web 安全的认知深度。
6.2 别被同名坑了:ArcMap、OSGB 里的“DOM”是另一回事
这里我必须插个题外话,因为在测绘和地理信息行业里,“DOM”是一个被我很多朋友问过太多遍的同名缩写。
在这个领域中,DOM 指 Digital Orthophoto Model,即数字正射影像模型,它和前端文档对象模型完全不是一个东西。比如有人在用 ArcMap 10.8 给影像数据做切片、或者做本地切片时,会搜索“DOM 切片”,得到的自然是地理信息领域的结果。那里的 DOM 是指经过几何校正后、带有真实地理坐标的航空或卫星影像,用来在地图软件里做底图或叠加分析,和浏览器、JavaScript、HTML 没任何关系。
还有一个常见混淆是 OSGB 和 DOM 的关系。OSGB 是 OpenSceneGraph Binary 的缩写,是倾斜摄影测量中生成的三维模型格式,带有完整的几何结构、纹理和层级细节;而测绘领域的 DOM 是二维正射影像,两者是不同类型的数据。在三维测图的工程流程里,通常会把倾斜摄影生成的 OSGB 模型和正射影像 DOM 配合使用,模型负责立体真实的城市面貌,影像负责平面精度和纹理参考。它们是互补关系,不是同一个东西。
作为一个前端开发者,碰到这种同名缩写,最好先确认语境再动手查资料,否则很容易被搜索引擎带偏。
6.3 我总结的 DOM 面试复盘清单
最后给准备面试的同学整理一份可以直接对着自测的清单。你能把下面这些问题不看资料回答出来,DOM 这块基本就过关了:
- 说一说 DOM 全称,以及文档、对象、模型分别代表什么?
- 浏览器是如何从 HTML 字节流构建 DOM 树的?
- DOM 和 CSSOM、Render Tree 之间是什么关系?
- CSS 为什么放在
<head>,JS 为什么放在</body>前? - 回流和重绘有什么区别?哪些属性会触发回流?
- 为什么说频繁操作 DOM 很慢?你有哪些减少回流的经验?
- 事件流分哪三个阶段?
addEventListener的第三个参数有什么作用? - 事件委托的原理是什么?为什么支持动态节点?
- 虚拟 DOM 解决了什么问题?它的结构长什么样?
- diff 算法为什么只比较同层?
key的作用是什么? - Vue 双端比较和 React Fiber 架构有什么本质区别?
- 什么是 DOM 型 XSS?如何防御?
这些问题大多数都能在这篇文章里找到答案。如果还有含糊的地方,建议把对应小节重读一遍,然后亲手上手写几个 demo 验证一下。
最后再说一点我的个人体会。DOM 这东西,你可以用 jQuery 或者 Vue、React 很多年都用不到深层原理,但它始终是面试的试金石,也是排查页面性能问题时最终定位的底层逻辑。我见过太多候选人把“虚拟 DOM 比真实 DOM 快”这句话挂在嘴边,但一问到回流重绘、事件流阶段,就立刻露出破绽。别做那种“背结论、不会推导”的人。真正理解了浏览器解析、渲染、事件传播这条链路之后,你会发现所有前端框架的设计都是围绕着 DOM 的高昂代价展开的。这份理解,值得你收藏起来反复看。