📘 第十章 构建Web内容的技术
📄 一句话概括

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 秒实验

  1. 在浏览器控制台里对任意元素输入 $0.parentNode,一路往上走,看能走到哪。
  2. 用 document.querySelectorAll('li') 数一下节点数,和 childNodes 的方式对比。
  3. 想一想:为什么改 innerHTML 比改 textContent 更容易触发重排?

常见误区

  1. 「DOM 就是 HTML」——HTML 是文本,DOM 是解析之后的对象树。
  2. 「id 比 class 快是因为 id 更特殊」——是因为浏览器给 id 建了索引。
  3. 「改样式和改结构代价一样」——改结构要重排,改颜色只重绘。
自测三问(点开对答案) 1. DOM 的节点能往哪三个方向走? —— 父节点、子节点、兄弟节点。 2. 为什么 `getElementById` 比 `getElementsByClassName` 快? —— 浏览器给 id 建了哈希索引,类选择器还是要遍历。 3. 改 DOM 结构为什么比改颜色贵? —— 改结构会触发重排,要重新计算所有元素的位置。

树是会变的,变它的往往是「算出来的内容」。这就得说到 Web 应用。

相关笔记