跳到主要内容

浏览器、DOM 与事件 — 页面是一棵会变的树

这一章讲三件事: 浏览器这个运行环境长什么样(网络、页面、运行笼子、历史); HTML 如何变成一棵可编程的树,JS 怎么读写它; 用户的行为(点击、按键、滚动)怎么变成事件流进你的程序。 原书这三章是进入浏览器世界的总门,我们合并成一章。

1. 浏览器是什么:一句话的三层

没有浏览器就没有 JavaScript——作者的原话还要更狠:「就算有,也不会有人理它」1

网络层:两台机器互相发比特,得先约好比特代表什么——协议(protocol)就是对通信方式的约定2

HTTP 负责「取回所谓的资源(网络上按名字取回的东西)」(如 GET /index.html HTTP/1.1)。HTTP 之下是 TCP:它把不可靠的比特流加工成「按序送达的双向管道」,每台机器靠端口号区分不同的监听者。

听的一方,行话叫服务器(守着应答的程序)。

来连的一方,叫客户端(发起请求的程序)。

页面层:万维网(注意,不等于互联网)= 「页面互相链接成网」。URL 三段:协议、服务器、路径;HTML 用标签给文本搭结构,而且解析极其容错——漏写的标签浏览器按标准化了的规则自动补齐3;

笼子层:浏览器里的 JS 不能碰你电脑上的文件,只能操作它嵌身的那一页。这叫沙箱(上锁的运行笼子);书里提醒你想象「一个焊了粗钢条笼子的沙箱」。难点是永恒的:给程序足够空间做有用的事,又不让它做危险的事——clipboard、联网都两头沾4

历史上还有一场浏览器战争:单一浏览器独大时,厂商就单方面发明特性,网站跟着用,其他浏览器只能追;2003 年前后是「兼容性黑暗时代」——几个互不兼容的平台,bug 还各不相同。如今的相对统一来之不易,而 Edge 于 2018 年改为 Chrome 内核后,作者留了一句警惕:市场份额集中,可能再次让单一厂商把它的 Web 观推给全世界5

2. DOM:一棵活树

浏览器解析 HTML,建起文档结构的模型;这个模型是 JS 在沙箱里的第一件玩具:它是活的——你一改,屏幕上的页面跟着变6

对象有类,节点也有型:nodeType 数字码——元素是 1、文本是 3、注释是 87。树的基本走线:parentNodechildNodes(以及只含元素版 children)、firstChild/lastChildpreviousSibling/nextSibling。找特定节点别走固定路径——那等于把文档结构焊死进程序;而且标签之间的空白也是文本节点:示例页面的 body 不是 3 个孩子,是 7 个。正路是按语义找:getElementsByTagName("a")getElementByIdgetElementsByClassName8

改树的动词:remove()appendChild()(接到末尾)、insertBefore(新, 旧)replaceChild(新, 旧)。有一条铁规则:一个节点在文档里只能存在一处——插入即移动。把第三段插到第一段前面,结果是「三、一、二」,不是复制9

两个著名的坑都在「列表是活的」:

  • getElementsByTagName 返回的是 live 列表,文档一变它就变。书里的删图换字程序从后往前循环,就为这个:从前往后删,列表会在你脚下滑走10;
  • 想要冻结的快照,Array.from(...) 转成真数组11

样式与属性侧:getAttribute/setAttribute 读写自定义属性(约定加 data- 前缀);class 因 JS 关键字之故叫 className布局是浏览器按类型与内容给每个元素算出的尺寸位置:offsetWidth/Height 含边框、clientWidth/Height 不含(3px 边框的段落:25 对 19);getBoundingClientRect 给精确屏幕坐标12

3. CSS:层叠出的最终样式

CSS 的 C 是 cascading(层叠):多条规则合成元素的最终样式;同一属性冲突时,后读到的规则赢;style 属性上的内联样式永远赢。规则越「具体」越优先:p.a 压过 p.a(特异性按 标签/类/ID 的数量与种类计);p > a 只管直系子节点,p a 管一切后代13

JS 侧的入口是 element.style 对象(连字符属性转驼峰:font-familyfontFamily);display: none隐藏元素的首选——比删掉好,想让它回来随时改回14。查找元素的现代入口 querySelectorAll("p > .animal") 直接复用 CSS 选择器这门小语言(返回值不 live,但仍不是真数组)15

布局是贵的:交替读写会拖慢百倍

浏览器尽量拖到最后一刻才重新计算布局;但你一读 offsetHeight 这类属性,它就被迫当场算一次。于是「改一下、读一下」交替的代码会逼出海量布局计算——书里的实测:往页面里拼 2000 像素宽的 X,朴素版(每加一个读一次宽度)32 毫秒,聪明版(先量一次、一次写够)1 毫秒16

4. 主走查:一次点击,一路上升

事件系统解决的问题是:别轮询——与其不停查「键按下了吗」,不如注册 handler,让系统在事件发生时来叫你17。注册用 addEventListener("click", fn)(一个节点可挂任意多个);onclick 属性也能挂,但每节点只容一个;想注销,removeEventListener 必须传当初那同一个函数值——所以 handler 要先起名字18

现在沿一棵具体的树走一遍点击:

DOM:<p> 段落里有 <button>按钮</button> </p>,两者都挂了 mousedown handler

① 鼠标按在按钮上 → 事件在 button 诞生,先调用 button 的 handler
② 不拦截的话,事件向上传播:段落 handler 接着被调用
③ 再向上:……一直到文档根,最后 window 也有机会响应

事件对象里:target = 事发的 button(原始节点,一路不变)
button 的 handler 里调 event.stopPropagation() → 段落 handler 不再被叫到

传播(propagation)= 事件从最内层节点出发,向外逐层告知祖先;更具体的 handler 先跑19。这带来一个极为常用的模式——宽网:给一列按钮的外层容器只挂一个 handler,用 event.target.nodeName == "BUTTON" 判断谁被点,而不是给每个按钮都挂。书里的三按钮例子:A、B、C 任意一个被点,document.body 上的 handler 借 target 报出名字20

另一个开关:preventDefault。多数事件先跑 handler、再执行默认行为(点链接跳转、按 ↓ 滚屏);handler 调 preventDefault 就能取消默认行为——可以实现自己的快捷键,也可以破坏用户预期,书里劝你非有正经理由别干。有些东西 JS 拦不住(Chrome 的 ctrl-W 关标签)21

5. 事件各论:每类一个坑

事件要点
keydown/keyup焦点决定落在哪个节点(没焦点时是 body);修饰键有 ctrlKey 等按住会连发;猜输入内容不可靠——安卓虚拟键盘不发、输入法多击合成,要内容请听 input 事件22
mousedown/up/clickclick 落在同时包含按下与松开的最具体节点(在 A 段按下、B 段松开,click 归它们共同的祖先)clientX/Y 相对窗口,pageX/Y 相对文档23
mousemove + 拖拽拖动时把 mousemove 挂在 window 上,拖出去也跟得上buttons(复数)=按着的键码之和(左1 右2 中4),顺序与 button 单数不同——「一致性不是浏览器接口的强项」24
touch 系列早期浏览器假装触摸=鼠标;假象不稳:无多键、离屏失联、多指并发touches 是列表(每指一组坐标);handler 里常要 preventDefault(防滚动+防鼠标事件双发)25
scroll做阅读进度条、懒加载preventDefault 拦不住滚动——handler 在滚动之后才被调26
focus/blur用 data-help 给输入框配提示(聚焦时显示的说明)不传播(父元素收不到)27
load / beforeunloadload 用于「等整页就绪」的初始化beforeunload 的自定义文案多数浏览器已不显示——被恶意站拿来骗人留页28

6. 页面卡了怎么办:worker 与防抖

事件处理跑在事件循环上——循环被占满,页面就又慢又卡:长 handler、或太密的短 handler 都会拖住一切交互29。两个药方:

  • web worker:一个同时运行的独立 JS 进程,不共享任何数据,只能互发消息(且消息必须能 JSON 化,对面拿到的是副本)——没有共享内存就没有数据竞争30;
  • 防抖(debouncing):mousemove/scroll/input 这类高频事件,别每响必应。两种模式:①等停顿再干——每次事件先 clearTimeout 再 setTimeout(500ms 后执行),连击时旧计时器不断作废;②限最短间隔——不到 250 毫秒不刷新,但事件持续也不丢31

动画则走 requestAnimationFrame:浏览器准备重绘时叫你(约每秒 60 次),让出主线程给浏览器去画屏、收输入;书里的猫沿椭圆飞舞的例子还演示了按「时间差」算增量——用两帧的间隔驱动角度,别的任务卡一下也不会让猫瞬移32

7. 作者的判断与证据

  • 「DOM 不是为 JS 设计的」:语言中立接口、还伺候 XML,于是有「节点类型用数字码表示」、NodeList 不是真数组这类「外来痕迹」痕迹;作者判句:与所用语言深度整合的接口,比跨语言一致的接口省时间33;
  • 「创建节点必须靠一连串副作用」:DOM 没有「一步造出带子节点的节点」的 API,代码因此冗长;缺陷不致命,JS 能自造抽象(elt 之类的工具函数、各种库)正是出路34;
  • 「统一可能再变独大」:对浏览器市场的政治判断,以 Edge 转 Chromium 为据5

判断(我们的,不是书里的): DOM 与事件这两章合起来的元技能是「把界面当状态函数」: 树是数据,事件是输入,重绘是输出。这个心智一直通到第 13 章像素编辑器的 「单一状态 + action 通道」架构,以及 React 等库的全部动机。 如果错,会错在: 直接命令式操作 DOM 的代码(高频游戏循环)有时确实更快, 「界面=状态函数」是可维护性的 optimum,不是性能的。

8. 边界与局限

  • CSS 只讲了够用的最小集,书里明说样式系统「复杂到值得单出一本书」;
  • 事件捕获(从根向下)阶段书里基本略过,只讲了冒泡方向;
  • Web Components、shadow DOM 等现代组件化设施不在本书;
  • 无障碍(focus 管理、aria)除 tabindex 一笔外未涉及。

9. 可带走的

  1. 没有浏览器就没有 JS;协议栈:HTTP 取资源,TCP 给「按序双向管道」,端口分流;
  2. 沙箱=钢条笼子:JS 只能碰自己那一页;
  3. DOM 是活树,改树即改屏;nodeType:元素 1、文本 3、注释 8;
  4. 空白也是文本节点;找节点用语义接口,别写死路径;
  5. 插入即移动;live 列表倒着删,或先 Array.from 冻结;
  6. 布局读写交替 = 性能灾难(32ms vs 1ms);先量好再一次写够;
  7. 层叠:后到赢、内联最赢、特异性定胜负;
  8. 事件从叶子向根传播;stopPropagation 拦、preventDefault 免默认、target 认源;
  9. 宽网模式:容器挂一个 handler,看 target 分发;
  10. 猜输入用 input 事件;click 归「按与放共同的最内层祖先」;
  11. 卡页面三件套:worker(同时干活的帮手)、防抖限频、rAF 让帧。

10. 原文地图

主题原书章原文位置
没有浏览器就没有 JSJavaScript and the Browsertext/16-fm-javascript-and-the-browser.txt:9(搜「Without browsers」)
协议与 TCP 管道同上text/16-fm-javascript-and-the-browser.txt:23(搜「protocol」) · :35(搜「port」) · :39(搜「two-way pipe」)
Web 与 URL同上text/16-fm-javascript-and-the-browser.txt:43(搜「World Wide Web」) · :49(搜「protocol server path」)
HTML 容错解析同上text/16-fm-javascript-and-the-browser.txt:95(搜「error-tolerant」)
script 标签与模块同上text/16-fm-javascript-and-the-browser.txt:122(搜「alert」) · :133(搜「type=」)
沙箱笼子同上text/16-fm-javascript-and-the-browser.txt:147(搜「sandboxing」) · :149(搜「hard part」)
浏览器战争与 Edge同上text/16-fm-javascript-and-the-browser.txt:157(搜「browser wars」) · :163(搜「wrapper around」)
DOM=活数据结构The Document Object Modeltext/17-fm-the-document-object-model.txt:11(搜「live data structure」)
nodeType 码同上text/17-fm-the-document-object-model.txt:48(搜「nodeType」)
语言中立接口批评同上text/17-fm-the-document-object-model.txt:56(搜「language-neutral」) · :58(搜「properly integrated」)
NodeList 不是数组同上text/17-fm-the-document-object-model.txt:60(搜「NodeList」)
副作用造节点的丑同上text/17-fm-the-document-object-model.txt:62(搜「side effects」)
空白文本节点 7 个孩子同上text/17-fm-the-document-object-model.txt:98(搜「whitespace」)
插入即移动同上text/17-fm-the-document-object-model.txt:132(搜「only one place」)
live 列表倒着删同上text/17-fm-the-document-object-model.txt:160(搜「live」)
Array.from 冻结同上text/17-fm-the-document-object-model.txt:164(搜「arrayish」)
data- 前缀与 className同上text/17-fm-the-document-object-model.txt:217(搜「data-」) · :219(搜「className」)
clientHeight 19 / offset 25同上text/17-fm-the-document-object-model.txt:229(搜「clientHeight」)
布局读写交替 32ms/1ms同上text/17-fm-the-document-object-model.txt:261(搜「naive」) · :267(搜「took 1 ms」)
层叠与特异性同上text/17-fm-the-document-object-model.txt:327(搜「precedence」) · :344(搜「specificity」)
querySelectorAll同上text/17-fm-the-document-object-model.txt:352(搜「querySelectorAll」)
rAF 与不冻结页面同上text/17-fm-the-document-object-model.txt:406(搜「60」) · :408(搜「freeze」)
轮询 vs 通知Handling Eventstext/18-fm-handling-events.txt:17(搜「polling」)
注销要同一函数值同上text/18-fm-handling-events.txt:61(搜「same function value」)
传播与 stopPropagation同上text/18-fm-handling-events.txt:87(搜「propagate」) · :89(搜「stopPropagation」)
宽网与 target同上text/18-fm-handling-events.txt:108(搜「wide net」) · :114(搜「nodeName」)
preventDefault 与拦不住的同上text/18-fm-handling-events.txt:125(搜「preventDefault」) · :140(搜「ctrl-W」)
keydown 连发与输入法同上text/18-fm-handling-events.txt:160(搜「repeats」) · :177(搜「input method editor」)
click 归共同祖先同上text/18-fm-handling-events.txt:189(搜「contained both」)
buttons 码序不一致同上text/18-fm-handling-events.txt:257(搜「buttons」) · :259(搜「consistency」)
触摸假象同上text/18-fm-handling-events.txt:263(搜「pretended」)
scroll 拦不住同上text/18-fm-handling-events.txt:332(搜「after the scrolling」)
focus/blur 不传播同上text/18-fm-handling-events.txt:338(搜「do not propagate」)
beforeunload 的教训同上text/18-fm-handling-events.txt:370(搜「weight-loss」)
worker 与消息副本同上text/18-fm-handling-events.txt:378(搜「web workers」) · :397(搜「copy」)
防抖两式同上text/18-fm-handling-events.txt:429(搜「debouncing」) · :445(搜「250」)

Footnotes

  1. 出处:「JavaScript and the Browser」第 9 段(text/16-fm-javascript-and-the-browser.txt:9,搜「Without browsers」)。

  2. 出处:「JavaScript and the Browser」第 23 段(text/16-fm-javascript-and-the-browser.txt:23,搜「protocol」)、第 35 段(text/16-fm-javascript-and-the-browser.txt:35,搜「port」)与第 39 段(text/16-fm-javascript-and-the-browser.txt:39,搜「two-way pipe」)。

  3. 出处:「JavaScript and the Browser」第 43 段(text/16-fm-javascript-and-the-browser.txt:43,搜「World Wide Web」)、第 53 段(text/16-fm-javascript-and-the-browser.txt:53,搜「protocol」)与第 95 段(text/16-fm-javascript-and-the-browser.txt:95,搜「error-tolerant」)。

  4. 出处:「JavaScript and the Browser」第 147 段(text/16-fm-javascript-and-the-browser.txt:147,搜「sandboxing」)与第 149 段(text/16-fm-javascript-and-the-browser.txt:149,搜「hard part」)。

  5. 出处:「JavaScript and the Browser」第 157 段(text/16-fm-javascript-and-the-browser.txt:157,搜「browser wars」)与第 163 段(text/16-fm-javascript-and-the-browser.txt:163,搜「wrapper around」)。 2

  6. 出处:「The Document Object Model」第 11 段(text/17-fm-the-document-object-model.txt:11,搜「live data structure」)。

  7. 出处:「The Document Object Model」第 48 段(text/17-fm-the-document-object-model.txt:48,搜「nodeType」)。

  8. 出处:「The Document Object Model」第 98 段(text/17-fm-the-document-object-model.txt:98,搜「whitespace」)与第 100 段(text/17-fm-the-document-object-model.txt:100,搜「first link」)。

  9. 出处:「The Document Object Model」第 132 段(text/17-fm-the-document-object-model.txt:132,搜「only one place」)。

  10. 出处:「The Document Object Model」第 160 段(text/17-fm-the-document-object-model.txt:160,搜「live」)。

  11. 出处:「The Document Object Model」第 162 段(text/17-fm-the-document-object-model.txt:162,搜「solid collection」)。

  12. 出处:「The Document Object Model」第 227 段(text/17-fm-the-document-object-model.txt:227,搜「offsetWidth」)与第 194 段(text/17-fm-the-document-object-model.txt:194,搜「19」)。

  13. 出处:「The Document Object Model」第 327 段(text/17-fm-the-document-object-model.txt:327,搜「higher precedence」)与第 344 段(text/17-fm-the-document-object-model.txt:344,搜「specificity」)。

  14. 出处:「The Document Object Model」第 297 段(text/17-fm-the-document-object-model.txt:297,搜「reveal them」)与第 311 段(text/17-fm-the-document-object-model.txt:311,搜「fontFamily」)。

  15. 出处:「The Document Object Model」第 352 段(text/17-fm-the-document-object-model.txt:352,搜「querySelectorAll」)与第 375 段(text/17-fm-the-document-object-model.txt:375,搜「not live」)。

  16. 出处:「The Document Object Model」第 247 段(text/17-fm-the-document-object-model.txt:247,搜「as long as they can」)与第 261 段(text/17-fm-the-document-object-model.txt:261,搜「naive」)。

  17. 出处:「Handling Events」第 17 段(text/18-fm-handling-events.txt:17,搜「polling」)与第 19 段(text/18-fm-handling-events.txt:19,搜「actively notify」)。

  18. 出处:「Handling Events」第 47 段(text/18-fm-handling-events.txt:47,搜「any number of handlers」)与第 61 段(text/18-fm-handling-events.txt:61,搜「same function value」)。

  19. 出处:「Handling Events」第 87 段(text/18-fm-handling-events.txt:87,搜「propagate」)。

  20. 出处:「Handling Events」第 108 段(text/18-fm-handling-events.txt:108,搜「wide net」)与第 115 段(text/18-fm-handling-events.txt:115,搜「nodeName」)。

  21. 出处:「Handling Events」第 125 段(text/18-fm-handling-events.txt:125,搜「preventDefault」)与第 138 段(text/18-fm-handling-events.txt:138,搜「really good reason」)、第 140 段(text/18-fm-handling-events.txt:140,搜「cannot be handled」)。

  22. 出处:「Handling Events」第 160 段(text/18-fm-handling-events.txt:160,搜「repeats」)、第 175 段(text/18-fm-handling-events.txt:175,搜「focus」)与第 177 段(text/18-fm-handling-events.txt:177,搜「input method editor」)。

  23. 出处:「Handling Events」第 189 段(text/18-fm-handling-events.txt:189,搜「contained both」)与第 193 段(text/18-fm-handling-events.txt:193,搜「clientX」)。

  24. 出处:「Handling Events」第 255 段(text/18-fm-handling-events.txt:255,搜「whole window」)、第 257 段(text/18-fm-handling-events.txt:257,搜「sum of the codes」)与第 259 段(text/18-fm-handling-events.txt:259,搜「consistency」)。

  25. 出处:「Handling Events」第 263 段(text/18-fm-handling-events.txt:263,搜「pretended」)与第 299 段(text/18-fm-handling-events.txt:299,搜「preventDefault in touch」)。

  26. 出处:「Handling Events」第 332 段(text/18-fm-handling-events.txt:332,搜「after the scrolling」)。

  27. 出处:「Handling Events」第 338 段(text/18-fm-handling-events.txt:338,搜「do not propagate」)。

  28. 出处:「Handling Events」第 366 段(text/18-fm-handling-events.txt:366,搜「load event」)与第 370 段(text/18-fm-handling-events.txt:370,搜「weight-loss」)。

  29. 出处:「Handling Events」第 376 段(text/18-fm-handling-events.txt:376,搜「slow and cumbersome」)。

  30. 出处:「Handling Events」第 378 段(text/18-fm-handling-events.txt:378,搜「web workers」)与第 397 段(text/18-fm-handling-events.txt:397,搜「copy of them」)。

  31. 出处:「Handling Events」第 429 段(text/18-fm-handling-events.txt:429,搜「debouncing」)与第 445 段(text/18-fm-handling-events.txt:445,搜「at least a certain length」)。

  32. 出处:「The Document Object Model」第 406 段(text/17-fm-the-document-object-model.txt:406,搜「60 per second」)与第 408 段(text/17-fm-the-document-object-model.txt:408,搜「freeze」)、第 410 段(text/17-fm-the-document-object-model.txt:410,搜「difference between the current time」)。

  33. 出处:「The Document Object Model」第 56 段(text/17-fm-the-document-object-model.txt:56,搜「language-neutral」)与第 58 段(text/17-fm-the-document-object-model.txt:58,搜「properly integrated」)。

  34. 出处:「The Document Object Model」第 62 段(text/17-fm-the-document-object-model.txt:62,搜「side effects」)与第 64 段(text/17-fm-the-document-object-model.txt:64,搜「fatal」)。