跳转至

构建 Web 应用 — 浏览器按什么顺序干活,JSX 把 DOM 藏到了哪

这一章讲三件事: 浏览器拿到一段 HTML 后,是按什么顺序把它变成页面的; 「事件」这种随时可能发生的东西,在单线程的世界里是怎么被排队和处理的; 以及 React 的 JSX 把前面那些手工 DOM 操作藏到了哪里、代价是什么。 读完你会明白:为什么一个卡死的页面恢复后会连响应你三次点击。

1. 顶层全景:一个 Web 应用只有两个阶段

原书把客户端 Web 应用的生命周期压成一张图(我们重画成文字版):1

  用户输入网址 / 点击链接
        │
        ▼
  浏览器向服务器发请求 ──→ 服务器处理,返回 HTML + CSS + JavaScript
        │
        ▼
  ┌─ 阶段一:建页(Page building)
  │    解析 HTML,一棵节点一个节点地搭出 DOM
  │    遇到 <script> → 暂停搭建,执行脚本(脚本能改 DOM),执行完继续搭
  │    HTML 耗尽 → 建页完成
  │
  └─ 阶段二:事件处理(Event handling)
       循环:看队列头部有没有事件 → 有就取出、执行绑定的处理器 → 没有就继续看
       (队列:用户点击/按键、网络响应、定时器……全排在同一列)
        │
        ▼
  用户关闭或离开页面 —— 生命周期结束

这里有两个贯穿全章的判断:

第一,Web 应用是 GUI 应用。 图形界面应用的宿命就是「大部分时间无所事事,偶尔响应一下用户」, 所以它的生命必然是「先搭界面,再进循环等事件」的两段式——桌面应用、手机应用同构。2

第二,两个阶段的边界是 DOM 的完成,而 JavaScript 在两个阶段都能插手。 建页时脚本能改 DOM(页面还没建完就能改),事件处理阶段脚本靠处理器持续改 DOM。 理解 DOM(见下)是理解一切的钥匙。

DOM(Document Object Model,文档对象模型) 是浏览器在内存里维护的一棵树, 树上的每个节点对应页面上的一个元素。HTML 是图纸,DOM 是照图纸盖出来的房子; JavaScript 不能直接改 HTML 文本,它改的是 DOM——改了 DOM,浏览器就把页面的显示更新过来。3

主线例子:一个「按键记事本」页面

书里用这个示例贯穿全章(我们照抄其骨架)。页面有一个列表 <ul id="event-list">, 脚本做三件事:定义一个往列表里加消息的函数;注册一个「DOM 建完」事件的处理器; 注册一个「按下键盘」事件的处理器;最后在脚本末尾直接加一条消息。4

在浏览器里打开这个页面,敲下 asdf 四个键,你会看到——注意顺序:

  End of script reached              ← 脚本末尾那句,建页阶段就执行了
  Event "DOMContentLoaded" fired     ← 建页完成的事件
  Event "keydown" fired with key = "a"
  Event "keydown" fired with key = "s"
  Event "keydown" fired with key = "d"
  Event "keydown" fired with key = "f"

第一句话出现在最前面,是本章第一个反直觉点:它证明脚本在建页阶段内就执行完了, 而不是等页面全部就绪。后面四行为什么是那个顺序,第 3 节回答。

2. 建页阶段:解析、DOM,以及 script 的「插队权」

2.1 HTML 怎样变成一棵树

浏览器拿到 HTML 后从头解析,一个元素一个元素地建节点。到第一个 <script> 时, 解析器身后的 DOM 长这样(文字版示意):5

  html
  ├── head
  │     └── (title、style 等已解析的节点)
  └── body
        └── ul#event-list          ← 脚本执行时,这个元素已经存在
              └── (脚本即将加进来的 li)

树的规矩:除根节点外每个节点恰有一个父节点;一个节点可以有任意多个孩子; 同一父节点下的孩子们互称兄弟。这套家谱词汇是 DOM API 的操作对象。

「脚本执行时它要找的元素必须已经存在」——这就是书上示例把 <ul> 写在 <script> 前面的原因, 也是无数「为什么 querySelector 返回 null」的答案。<script> 的默认行为是阻塞: 见到它就停下解析、执行完再继续(可以用 async、defer、type="module" 等属性改变, 书指向 MDN,不展开)。6

2.2 HTML ≠ DOM:浏览器会替你改图纸

一个容易忽略的要点:HTML 和 DOM 关联紧密,但不是同一个东西。 HTML 只是初版图纸,浏览器发现图纸有错会静默修正。书里给了一种典型错误: 有人把该放进 body 的内容段落写进了 head——浏览器会不声不响地把它挪进 body, 构出它认为合法的 DOM。原书一句话点破:把 HTML 当成图纸,把 DOM 当成房子。7

顺带一个边界的知识(书里边栏):HTML 和 DOM 各有一份「活标准」, 而且 DOM 标准是中立于 HTML 的——矢量图格式 SVG 用的也是同一套 DOM。 实战上,MDN 文档比规范原文更有用。8

2.3 脚本执行时发生了什么

回到主线例子的脚本,逐行走一遍:9

const eventList = document.querySelector("#event-list");  // ① 按 CSS 选择器找到那个 ul

const appendToEventList = (message) => {                   // ② 定义函数(此时不执行)
  const messageElement = document.createElement("li");     //    造一个新节点
  messageElement.textContent = message;                    //    填文本
  eventList.appendChild(messageElement);                   //    挂到 ul 下
};

document.addEventListener("DOMContentLoaded", () => {      // ③ 登记:DOM 建完时叫我
  appendToEventList('Event "DOMContentLoaded" fired');
});
document.addEventListener("keydown", (evt) => {            // ④ 登记:有按键时叫我
  appendToEventList(`Event "keydown" fired with key = "${evt.key}"`);
});

appendToEventList("End of script reached");                // ⑤ 立刻执行:加第一条

三件事分清楚:document.querySelector 返回的是 DOM 树上的现成节点(第 ① 步); document.createElement 造的是游离的新节点(第 ② 步里),appendChild 把它挂上树; 第 ③④ 步的 addEventListener 只是登记,处理器此刻不跑;第 ⑤ 步是唯一在建页阶段就立刻执行的加节点操作。

脚本跑完后,浏览器接着解析剩余 HTML,然后进入第二阶段。一个重要细节: 脚本之间共享全局状态——一个 script 标签里定义的顶层变量,后面的 script 标签直接用, 因为它们都活在同一个全局 window 对象上。10

3. 事件处理:一条队列、一个线程、三次点击之谜

3.1 银排队列

浏览器执行环境的核心纪律叫单线程执行模型:任何时刻只有一段代码在跑。 原书用的比方是银行柜台——只有一个窗口,客户(事件)排成一列,办完一个办下一个; 只要有一个客户在窗口里做「全年财务规划」,全队卡住。11

但用户不会乖乖按节奏触发事件。浏览器需要一个地方暂存「已经发生但还没处理」的事件—— 事件队列(event queue)。所有事件,不管来源(用户按键、网络响应、定时器),都按被探测到的先后 进同一条队列。处理循环只有三行逻辑:12

  1. 看队列头部
  2. 空 → 继续看(空转等待)
  3. 有 → 取出,执行绑定的处理器;处理期间,其他事件在队列里等

「卡死的页面恢复后连响应三次点击」之谜,在这里解开: 把事件放进队列的机制, 独立于处理事件的线程——它在外面探测「用户点了」,只管往里放。 页面被一段昂贵的 JavaScript 卡死时,点击照样被探测、照样入队; 等线程空出来,三个点击事件挨个取出、挨个执行,于是按钮的处理器连跑三次。13

推论立刻出现:处理器跑多久,页面就卡多久。 处理器的执行时间直接等于界面的无响应时间——这是「不要阻塞线程」在第 1 章之后的第二次出现, 也是 Web 性能里最硬的一条纪律。

3.2 事件的三个来源与异步的本性

会发生的事件不限于用户操作,原书列了四类:浏览器事件(页面加载完成、即将卸载)、 网络事件(fetch 的响应)、用户事件(点击、移动、按键)、定时器事件(超时到期、promise 落定)。 它们共同的脾气是异步(asynchronous):发生的时间和顺序不受你控制—— 「强迫用户按某个顺序按键」是做不到的。14

由此得出一个统计性的事实:一个典型 Web 应用里,绝大部分运行的代码都跑在事件处理器里。 这句话值得记住,因为它解释了为什么这本书用四章讲函数——事件处理器就是回调, 回调就是函数作为一等对象的应用。

3.3 注册处理器,以及一个事件的小魔术

addEventListener(事件名, 处理器) 是登记的标准姿势,主线例子的 ③④ 步已经演示。 处理器是普通的函数值——又一次,「函数是值」在干活。15

有个细节初读会意外:keydown 事件是从键盘焦点所在元素发出的, 可书上的处理器挂在 document 上,为什么在输入框里按键也能触发? 答案是 DOM 的冒泡(bubbling):事件从发生它的节点出发,逐级向上经过父节点直到 document, 沿途的每个节点都有机会拦下它。所以「在哪里注册」和「在哪里发生」可以不是同一个节点。 (冒泡与它的近亲「捕获」的完整规则,原书安排在后续章;本章只要知道「事件会向上冒」这一件事。16)

4. JSX:把「操作 DOM」改写成「描述状态」

到此为止,页面内容是直接写 HTML、用 DOM API 手工改的。2010 年代中期,React 带来另一种姿势: JSX——一种把 HTML 语法嵌进 JavaScript 的语言扩展。看这一行:17

const link = <a href="https://manning.com">Manning</a>;

它不是 HTML 字符串,而是一个值。JSX 编译成普通 JavaScript 后,标签变成了函数调用:

const link = _jsx("a", { href: "https://manning.com", children: "Manning" });

_jsx 来自所选运行时(React 是原创者,也有别的实现):参数是标签名和一个属性对象, 返回值是页面的一个「描述对象」。——这一步看清,JSX 的神秘感就消失了: 它只是一段生成普通对象的语法糖。

用 JSX 重写主线例子,形态大变:不再有 createElement/appendChild/addEventListener, 而是一个「组件」函数——输入是状态(一个事件字符串数组),输出是「页面应该长什么样」:18

import React, { useState } from "react";

export default function EventList() {
  const [events, setEvents] = useState(["Initial event"]);   // 状态:事件列表
  return (
    <ul id="event-list">
      {events.map((str) => <li>{str}</li>)}                  {/* 每个事件一个条目 */}
      <li>
        <input onKeyDown={(evt) => {                          {/* 按键处理器 */}
          setEvents(events.concat(`keydown: ${evt.key}`));    {/* 状态更新 */}
        }} />
      </li>
    </ul>
  );
}

注意按键处理器挂在 <input> 上而不是 document(在 React 里挂 document 需要额外的 hook,书中略去)。 神奇之处:对数组的任何修改——加、删、改——React 会自动把 DOM 更新到和你的输出一致。 你从此只思考「状态 → 界面」,不思考「哪个节点插哪」。书里说得很直白:没有 createElement, 没有 appendChild,没有 addEventListener。19

4.1 虚拟 DOM:这份省心的价格

React 凭什么做到?它在内部维护一棵虚拟 DOM——各组件输出拼成的「页面应有的样子」, 一份自己的树。每次状态变化,它重新渲染出新的虚拟 DOM,和旧版做一次比对 (这个过程叫 reconciliation,调和),只把差异部分应用到真 DOM 上。20

这套机制统治了 Web 开发,但原书立刻记下它的代价,而且这代价正是第 1 章「代码膨胀/SSR」主题的落地:

  • 用 JavaScript 对象描述一个节点,比等价的 HTML 标签更大、解析更贵;
  • 框架自身的代码量不小,React 本身要先下载、先执行;
  • 打包出来的 bundle 可能包含用户此刻看不到的页面、甚至别的页面的代码。

对策三件套(原书的「忍者责任」清单):考虑更轻的框架(如 Solid);把 bundle 切块,首屏用不到的延后加载; 能用 SSR 就用 SSR——同一份代码先在服务器上渲染成完整 HTML,浏览器先见内容、后跑脚本。21

5. 作者的判断与证据

本章的机制性内容(解析阻塞、队列行为、JSX 编译形态)全部可由读者亲手复现,原书给出了完整的 HTML 与命令行输出作为证据,包括「End of script reached 出现在 DOMContentLoaded 之前」这个 决定阶段划分的观察。22

作者立场(当立场读):

  • 「框架要负责任地用」及 Solid/SSR 建议,是作者的价值判断,React 生态的现实惯例未必一致;
  • 「virtual DOM 节省了直接操作 DOM 的麻烦」是社区共识级的说法,但「直接操作 DOM 一定慢」 是更早时代的经验,原书没有卷入这场争论,只陈述「比对后只更新差异」的机制本身。

6. 边界与局限

  • 事件系统只讲了冒泡的一句。 捕获阶段、stopPropagation、自定义事件, 原书都标注在后续章——MEAP 里没有。
  • React 示例以「看懂机制」为限。 useState 为什么返回数组、hook 的规则, 都不在讲;别拿这章当 React 入门。
  • <script> 的 async/defer/module 差异 只给了名字和 MDN 链接,没有行为对比表。
  • 「渲染不是 JavaScript 做的」 一句在后续章节(微任务)里还会回来,本章先不展开。

7. 可带走的(合上这一章,你应该有这八条)

  1. Web 应用生命 = 建页 + 事件处理;分界线是 DOM 搭完,而脚本在建页中途就会插队执行。
  2. DOM 是内存里的树,HTML 只是图纸;浏览器会静默修正图纸的错误;脚本只能改 DOM,不能改 HTML。
  3. <script> 默认阻塞解析,且脚本执行时只能访问它之前的节点——顺序写反,querySelector 给你 null。
  4. 事件全部进同一条队列,放入队列的机制独立于执行线程——所以卡死时的点击会被攒着,恢复后连响应。
  5. 处理器执行时间 = 页面卡死时间;单线程模型的第一纪律再次生效。
  6. 处理器就是回调,而回调是一等函数——这就是这本书用四章讲函数的原因。
  7. JSX 编译成普通函数调用(_jsx(标签, 属性)),组件 = 「状态 → 界面描述」的函数。
  8. 虚拟 DOM 比对后只更新差异;代价是 JS 体积与启动时间,对策是轻框架、切包、SSR。

9. 深读一:建页阶段的逐步剧本,从字节到树

第 2 节讲了原则和一次脚本执行,这里把浏览器从「收到响应」到「DOM 就绪」的完整剧本 按时间轴铺开——时间轴上每个刻度,都对应一个可以观察的现象。

  t0  浏览器收到 HTML 的第一块字节,解析器开工
  t1  逐元素建节点:<!doctype>、<html>、<head>、<title>、<style>……
  t2  遇到 <script>(无 async/defer)──► 解析暂停,DOM 建设停工
  t3  脚本执行:能读到 t2 之前已建好的全部节点
  t4  脚本结束(可能改了 DOM)──► 解析复工,接着 t1 的节奏往下建
  t5  HTML 耗尽:DOM 建完,DOMContentLoaded 事件入队等待处理
  t6  进入事件处理阶段

三个可以亲手验证的推论:

  • 脚本执行时,后面的元素还不存在:把 querySelector("#event-list") 移到 <ul> 之前,得到 null——顺序就是生命线;
  • 脚本之间共享全局状态:同一页面两个 script 标签,后一个能读前一个的顶层变量, 因为它们都活在同一个全局 window 对象上——这也解释了为什么老代码里 「先引 jQuery 再引插件」的顺序不能换;1
  • DOMContentLoaded 是事件:它同样进事件队列——所以注册得晚于它触发,处理器就不执行。 「事件随时发生,处理器必须提前登记」,本章的纪律对浏览器自己的事件同样成立。

9.1 HTML 纠错的完整样例

书里的 Figure 4.4 给了一种典型错误:把内容段落 <p> 写进了 <head>。 浏览器的处理不是报错,而是挪:head 只该放页面元信息(标题、字符集、外部样式表 和脚本的引用),内容属于 body——解析器发现图纸不对,就按自己的理解重画, 段落被悄悄放进 body。「浏览器会修你的 HTML」是把双刃剑:页面能跑, 但 DOM 和你写的 HTML 已经不是一回事,调试时别以为它们相等。2

顺带一条边栏级的知识(书里给了):HTML 和 DOM 各有一份活标准(living standard), 而且 DOM 标准是平台中立的——连矢量图格式 SVG 都复用它。实战上 MDN 比规范原文 好用,但知道「DOM ≠ HTML 的附属品」有助于理解为什么它有自己的规范。

10. 深读二:事件队列的完整运转,与「冻结页面」的微观解剖

10.1 队列的三行循环,配一张时刻表

三行循环(看队头、空则再看、有则取出执行)配上书里的按键例子,时刻表长这样:3

  时刻  发生的事                       队列(头→尾)            线程在干嘛
  ─────────────────────────────────────────────────────────────────────
  t0    页面加载完成,进入事件处理      [ DOMContentLoaded ]    空转检查
  t1    取出 DOMContentLoaded          [ ]                     执行它的处理器
  t2    用户敲下 a                     [ keydown(a) ]          空转检查
  t3    取出 keydown(a)                [ ]                     执行处理器:列表加一行
  t4    敲 s、d、f(假设很快)          [ keydown(s,d,f) ]      空转检查
  t5    逐个取出执行                   ……                      列表连加三行

时刻 t4 一行值得停顿:三个按键事件排成一队等处理,哪怕它们几乎同时发生。 「单线程」不是「同时只处理一个事件」的优雅说法,而是硬约束:处理器执行期间, 队列只进不出。

10.2 冻结的微观解剖:三次点击的完整时间线

把书里的「卡死按钮」例子展开成时间线:4

  0ms    用户点击 ──► 事件入队 [click] ──► 处理器开始执行一段 5 秒的昂贵计算
  50ms   用户再点 ──► 入队 [click]        (线程仍卡在第一个处理器里)
  80ms   又点   ──► 入队 [click]          (入队机制独立运行,不受线程卡住影响)
  5000ms 第一个处理器终于跑完 ──► 队列出队:第二个 click 执行
  ~0ms   第二个跑完 ──► 第三个 click 执行
  结果:用户明明「点了一下没反应又点了两下」,最后按钮响应了三次

冻结期间页面为什么不响应任何交互(滚动、别的按钮)?因为所有事件的处理器 都等同一个线程。这条机制同时解释了 Web 性能的第一定律: 处理器的时长 = 页面无响应的时长——「不要阻塞线程」在浏览器侧的精确含义。5

10.3 事件的四类清单,与异步的定义

书里把事件分了四类,值得背下来作为「什么东西会触发我的处理器」的检查表:6

类别 例子 来源
浏览器事件 页面加载完成、即将卸载 浏览器自身
网络事件 fetch 的响应到达 服务器/网络栈
用户事件 点击、移动、按键 输入设备
定时器事件 超时到期、promise 落定 定时器/promise 机制

它们被统称为异步的根据:发生的时间不可预测、顺序不可预测 (「强迫用户按某个顺序按键」做不到)。原书给了一个统计性判断: 典型 Web 应用里绝大部分运行的代码都跑在事件处理器里—— 这就是为什么「事件处理器 = 回调 = 一等函数的应用」这条链是本书的心脏。

11. 深读三:JSX 编译的完整机制,与虚拟 DOM 的账

11.1 从标签到函数调用,再进一步

第 4 节给了 <a> 的一行编译结果,把 React 版「事件列表组件」的编译形态 在脑子里过一遍(书里没有逐行展开编译产物,但机制相同):7

  <ul id="event-list">                        _jsx("ul", { id: "event-list",
    {events.map((str) => (                        children: events.map((str) =>
      <li>{str}</li>         ──编译──►              _jsx("li", { children: str })
    ))}                                          )
    <li><input onKeyDown={…} />                 _jsx("li", { children: _jsx("input",
  </ul>                                               { onKeyDown: … }) })
                                                    })
                                                  })

两个观察:其一,JSX 嵌套 = 函数调用嵌套——HTML 的层级就是 _jsx 调用的层级, 树的形状在编译期就定好了;其二,{…} 里的 JavaScript 原样保留—— events.map(...) 是普通数组方法,onKeyDown 只是个属性值。 读懂这两条,React 模板里的任何写法你都能预测它的运行形态。8

11.2 虚拟 DOM 调和的完整账

reconciliation 的过程,书里给了三步,展开成一次具体的状态更新:

  状态变化:setEvents([...events, "keydown: x"])
      │
      ▼
  ① React 重新执行组件函数,得到一棵新的虚拟 DOM 树(内存里的描述对象,不是真 DOM)
  ② 和旧虚拟 DOM 树逐节点比对(调和):ul 没变、li 列表多了一项、input 没变
  ③ 把差异应用到真 DOM:只对「多出来的那个 li」做一次真实的 DOM 插入

账本的另一面(书里的三宗代价,配数字感):JS 对象描述节点比 HTML 标签体积更大—— 一个 <div class="x"> 标签十几个字节,对应的对象描述含标签名、属性表、子节点数组, 几十字节起;框架自身是一大段必须先下载执行的代码;打包器若不做代码分割, 首屏还会带上别的页面的代码。三个对策按性价比排:SSR(根治「空页面等 JS」)、 代码分割(根治「带了一堆用不到的」)、轻量框架(根治「框架本身太大」, 书里点了 Solid 的名)。9

12. 原文地图

主题 原书章 原文位置
生命周期两阶段 Chapter 4: Building web applications text/07-ch04-chapter-4-building-web-applications.txt:23(搜「lifecycle」)
按键记事本示例与输出顺序 同上 :51(搜「Listing 4.1」) · :107(搜「End of script reached」)
HTML 解析与 DOM 树 同上 :119(搜「tree representing the contents」) · :145(搜「exactly one parent」)
script 阻塞与 async/defer 同上 :123(搜「stop parsing the document」)
HTML 与 DOM 的区别及浏览器纠错 同上 :150(搜「blueprint」) · :158(搜「silently fixes」)
脚本执行逐行 walk-through 同上 :180(搜「Executing JavaScript code」)
全局状态跨 script 持久 同上 :216(搜「persists in the meantime」)
单线程与银行比方 同上 :235(搜「single-threaded execution model」)
事件队列三行循环 同上 :254(搜「event queue」) · :256(搜「checks the head」)
三次点击之谜 同上 :269(搜「external to the page-building」)
事件四类与异步定义 同上 :277(搜「EVENTS ARE ASYNCHRONOUS」)
addEventListener 与冒泡 同上 :294(搜「Registering event handlers」) · :314(搜「bubbling」)
JSX 编译为 _jsx 调用 同上 :321(搜「Writing interactive HTML with JSX」) · :330(搜「compiles to JavaScript」)
事件列表组件重写 同上 :347(搜「Listing 4.3」)
虚拟 DOM 与 reconciliation 同上 :387(搜「virtual DOM」)
框架的三宗代价与对策 同上 :392(搜「come at a cost」)
全章总结 同上 :410(搜「Summary」)

  1. 出处:「Chapter 4: Building web applications」第 214–224 段(text/07-ch04-chapter-4-building-web-applications.txt:217,搜「persists in the meantime」)。全局状态跨 script 标签共享的机制:全局 window 对象在整个页面生命周期内活着。 ↩↩

  2. 出处:「Chapter 4: Building web applications」第 150–178 段(text/07-ch04-chapter-4-building-web-applications.txt:161,搜「silently fixes it」)。head 误放段落的纠错例子与 head 的职责说明(第 159–161 段)。 ↩↩

  3. 出处:「Chapter 4: Building web applications」第 246–268 段(text/07-ch04-chapter-4-building-web-applications.txt:257,搜「checks the head of the event queue」)。三行处理流程原文;时刻表按 Listing 4.1 的输出(:107,搜「End of script reached」)构造。 ↩↩

  4. 出处:「Chapter 4: Building web applications」第 269–275 段(text/07-ch04-chapter-4-building-web-applications.txt:269,搜「external to」)。入队机制独立于处理线程的原文;「it responds to all three clicks」见第 274–275 段。 ↩↩

  5. 出处:「Chapter 4: Building web applications」第 265–268 段(text/07-ch04-chapter-4-building-web-applications.txt:265,搜「extra careful about the amount of time」)。处理器时长与无响应时间的等式。 ↩↩

  6. 出处:「Chapter 4: Building web applications」第 277–291 段(text/07-ch04-chapter-4-building-web-applications.txt:282,搜「Browser events」)。事件四类清单;「vast majority」的统计判断见第 288–290 段。 ↩↩

  7. 出处:「Chapter 4: Building web applications」第 321–341 段(text/07-ch04-chapter-4-building-web-applications.txt:330,搜「converted to objects」)。JSX 编译为 _jsx 调用的机制与 Listing 4.3 的组件代码(:347,搜「Listing 4.3」)。 ↩↩

  8. 出处:「Chapter 4: Building web applications」第 330–341 段(text/07-ch04-chapter-4-building-web-applications.txt:330,搜「compiles to JavaScript」)。「JSX ultimately compiles to JavaScript」与「a straightforward object representation」。 ↩↩

  9. 出处:「Chapter 4: Building web applications」第 387–408 段(text/07-ch04-chapter-4-building-web-applications.txt:392,搜「come at a cost」)。三宗代价清单与三对策;Solid 的点名见第 405–406 段。 ↩↩

  10. 出处:「Chapter 4: Building web applications」第 214–220 段(text/07-ch04-chapter-4-building-web-applications.txt:217,搜「persists in the meantime」)。全局状态跨 script 标签共享的机制(全局 window 对象)。 ↩

  11. 出处:「Chapter 4: Building web applications」第 235–246 段(text/07-ch04-chapter-4-building-web-applications.txt:237,搜「single-threaded execution model」)。银行柜台比方原文。 ↩

  12. 出处:「Chapter 4: Building web applications」第 246–268 段(text/07-ch04-chapter-4-building-web-applications.txt:247,搜「event queue」)。队列的角色与三行处理流程(第 256–263 段)。 ↩

  13. 出处:「Chapter 4: Building web applications」第 269–275 段(text/07-ch04-chapter-4-building-web-applications.txt:269,搜「It’s important to note that the browser mechanism」)。入队机制独立于处理线程——「连点三下、响应三次」的解释原文。 ↩

  14. 出处:「Chapter 4: Building web applications」第 277–291 段(text/07-ch04-chapter-4-building-web-applications.txt:277,搜「EVENTS ARE ASYNCHRONOUS」)。事件四类清单与异步定义。 ↩

  15. 出处:「Chapter 4: Building web applications」第 292–313 段(text/07-ch04-chapter-4-building-web-applications.txt:298,搜「addEventListener method」)。注册处理器即「通知浏览器你有兴趣」。 ↩

  16. 出处:「Chapter 4: Building web applications」第 314–319 段(text/07-ch04-chapter-4-building-web-applications.txt:319,搜「bubbling」)。事件可从不同节点拦截、冒泡的引入;捕获在后续章。 ↩

  17. 出处:「Chapter 4: Building web applications」第 321–341 段(text/07-ch04-chapter-4-building-web-applications.txt:328,搜「const link = <a href」)。JSX 的一行示例与其「值」的身份。 ↩

  18. 出处:「Chapter 4: Building web applications」第 330–341 段(text/07-ch04-chapter-4-building-web-applications.txt:330,搜「converted to objects」)。HTML 标签编译为对象、_jsx 函数调用形态。 ↩

  19. 出处:「Chapter 4: Building web applications」第 343–385 段(text/07-ch04-chapter-4-building-web-applications.txt:347,搜「Listing 4.3」)。事件列表组件重写与「There's no createElement, no appendChild, no addEventListener」的原文(第 380 段)。 ↩

  20. 出处:「Chapter 4: Building web applications」第 387–391 段(text/07-ch04-chapter-4-building-web-applications.txt:387,搜「React provides this simple and elegant developer experience」)。虚拟 DOM 定义与 reconciliation 的机制描述。 ↩

  21. 出处:「Chapter 4: Building web applications」第 392–408 段(text/07-ch04-chapter-4-building-web-applications.txt:392,搜「come at a cost」)。三宗代价清单与 Solid/切包/SSR 对策。 ↩

  22. 出处:「Chapter 4: Building web applications」第 107–115 段(text/07-ch04-chapter-4-building-web-applications.txt:107,搜「you should see this output」)。输出顺序作为阶段划分的可复现证据。 ↩