DOM 把 HTML 变成一棵树:每个标签是一个节点,每个节点能从父、子、兄弟三个方向走。 改页面就是改这棵树——脚本改的就是它。
上一节说「脚本改页面」,改的是什么?就是这一节这棵树。 看懂树,才知道为什么有些选择器找不到元素。
一、树是从字节解析出来的
浏览器收到 HTML 之后先做一遍解析,把标签的嵌套关系变成一棵树。
最上面是 #document,下面是 <html>,再往下是 <head> 和 <body>。
Demo 用一个最简的解析器复现了这个过程:一段三十行的页面,解析出 14 个节点、最大深度 5 层。
真实页面动辄几千个节点,深十几二十层。
为什么是树不是列表?因为标签是嵌套的。<ul> 里包着 <li>,<li> 里又包着文字。
这种「包含」关系天然就是一棵树,而树恰好能让「我在哪」这件事有明确答案。
二、三个方向,三种走法
往上走:parentNode,拿到父节点。往下走:childNodes,拿到子节点列表。
横着走:nextSibling/previousSibling,拿到兄弟。
三个方向都能走,意味着从任何一个节点都能走到任何另一个节点。
所以用「父元素的第 2 个子元素的第 1 个孩子」这种路径可以精确定位——
这就是「文档对象模型」这个名字的来历:把文档变成可编程的对象。
还有一组更好用的:querySelector 吃一个 CSS 选择器,返回第一个命中的节点。
它把 10.1.3 那套选择器直接复用到了脚本里。
三、找节点的代价不一样
Demo 里按 id 找用了 8 步,按 class 找用了 9 步。差距看着不大,因为页面只有 14 个节点。
真实的浏览器不会傻扫:它给 id 建了哈希索引,getElementById 几乎一步到位。
而 getElementsByClassName 还是要遍历——因为一个类可以命中很多元素。
所以「id 比 class 快」的真正原因是索引,不是 id 更「重要」。
这也是为什么早期的优化建议里总说「少用深层选择器」——每多一层,扫描的面就大一圈。
改树也有代价:改结构会触发重排(reflow),改颜色只触发重绘(repaint)。
重排要重新算所有元素的位置,贵得多。这一点本机没有浏览器,测不出数字,只把机制说清楚。
四、动手 Demo:把一棵树建出来,再数走到目标要几步
用一个极简解析器把页面造成树,数节点总数和最大深度; 然后分别按 id 和按 class 做一次深度优先遍历,数各访问了多少个节点。
class Node(object):
def __init__(self, tag, attrs, parent):
self.tag, self.attrs, self.parent = tag, dict(attrs), parent
self.kids, self.text = [], []
def walk(n, depth=1):
nodes.append((n, depth))
for k in n.kids:
walk(k, depth + 1)
实跑输出:
--------------------------------------------------------------------
DOM 就是一棵树:找节点要走多少步
--------------------------------------------------------------------
节点总数 14 个(含 #document)
最大深度 5 层
最深的标签 strong/li/li
按 id 找 `top`:深度优先遍历,边找边数
逐节点找 id=top 访问 8 个节点后找到
它的路径 #document ← html ← body ← h1
按 class 找 `lead`:同样遍历
逐节点找 class=lead 访问 9 个节点后找到
真实的浏览器不会这么傻找:它给 id 建了索引(哈希表),
所以 `getElementById` 几乎一步到位,而 `getElementsByClassName` 还是要扫。
这就是二者性能差的来源——**不是因为 id 更「重要」,是因为它被建了索引**。
树上每个节点能去的地方 父、子、兄弟
正因为三个方向都能走,你才能用「父元素的第 2 个子元素的第 1 个孩子」
这种路径精确定位到任何一个节点——这就是「文档对象模型」这个名字的来历。
14 个节点、最大深度 5 层,最深的是 strong 和两个 li。
一个三十行的页面就有五层,真实页面十几层很常见。
按 id 找 top,访问 8 个节点后命中,路径是 #document ← html ← body ← h1。
这条路就是「父节点往上看」的结果。
按 class 找 lead,访问 9 个节点。多一个是因为它在文档里更靠后。
这两个数在真实页面上会拉得很开:id 走索引一步到位,class 要扫完整个子树。
差距来自有没有建索引。
30 秒实验
- 在浏览器控制台里对任意元素输入
$0.parentNode,一路往上走,看能走到哪。 - 用
document.querySelectorAll('li')数一下节点数,和childNodes的方式对比。 - 想一想:为什么改
innerHTML比改textContent更容易触发重排?
常见误区
- 「DOM 就是 HTML」——HTML 是文本,DOM 是解析之后的对象树。
- 「id 比 class 快是因为 id 更特殊」——是因为浏览器给 id 建了索引。
- 「改样式和改结构代价一样」——改结构要重排,改颜色只重绘。
自测三问(点开对答案)
1. DOM 的节点能往哪三个方向走? —— 父节点、子节点、兄弟节点。 2. 为什么 `getElementById` 比 `getElementsByClassName` 快? —— 浏览器给 id 建了哈希索引,类选择器还是要遍历。 3. 改 DOM 结构为什么比改颜色贵? —— 改结构会触发重排,要重新计算所有元素的位置。树是会变的,变它的往往是「算出来的内容」。这就得说到 Web 应用。
- ☐ (本节完,下一节 10.3.1 通过Web提供功能的Web应用)