Chapter 06

自测:三层题库

前五章建立了完整的心智模型。这一章用三层题检验它真的进了脑子,而不只是“读得很顺”。重点是最后的判别层——它逼你在跨章的新场景里做选择,这才是迁移。

怎么用这一章

  • 三层梯度:概念层(回忆)→ 原理层(分析机制)→ 应用判别层(跨章场景)
  • 所有答案集中在文末一个折叠块里。每题先自己写,再展开——“瞄一眼答案”会把检验变成又一次再读
  • 判别层答不出,回对应章节重读;那才是模型没扎实的地方
概念层 · 回忆与理解 对应 01–02 · 题 1–4 原理层 · 分析机制 对应 02–05 · 题 5–10 判别层 S1–S5 迁移度 ↑ 回忆
图 6.1三层不是简单的“难度递增”,是认知层级递增:从“能复述”到“能讲机制”再到“能在新场景里判别选型”。注意:只有顶层判别题能检验迁移——前两层答得顺,恰恰是该警惕“流畅感错觉”的时候。

A概念层(对应 01–02)

  1. “声明式”把原本属于你的哪一项工作转移给了 React?(§1.1)
  2. const el = <h1>Hi</h1> 求值后,el 是什么?页面上出现 <h1> 了吗?(§1.3)
  3. props 为什么是只读的?子组件想改变父组件的数据,唯一的合法途径是什么?(§1.4)
  4. “一个组件被重渲染”和“它对应的真实 DOM 被修改”是同一件事吗?(§2.1)

B原理层(对应 02–05)

  1. 协调把通用 O(n³) 树 diff 降到 O(n),靠的两条启发式假设各是什么?(§2.2)
  2. 本帧 count = 0,连写三次 setCount(count + 1),结果为什么只到 1?(§3.2)
  3. arr.push(x) 之后 setArr(arr),界面为什么纹丝不动?React 用什么判断“变没变”?(§3.3)
  4. 为什么 Hook 不能写在 if 或循环里?用“调用顺序认领槽”来解释。(§4.1)
  5. useEffect(fn, [dep]) 的 cleanup 函数在哪两个时刻运行?(§4.3)
  6. React Compiler(2025-10 GA)让哪三个 API 基本不必再手写?它改变 state → UI 的语义吗?(§5.5)

C应用判别层 · 跨章场景

每道题都横跨至少两章。先判断“涉及哪几章的哪个概念”,再给方案。这是整份教程真正要检验的能力。

场景 \ 章节 01 02 03 04 05 S1 · 列表串状态 S2 · 定时器不动 S3 · 选型权衡 S4 · ref 还是 state S5 · RSC 切分
图 6.2五道判别场景与章节的对应。注意:每道题都落在两列上——现实问题从不按章节边界出现。若某道题你只想到一章,多半漏看了另一半;答案里会点出缺的那半。
  1. S1 · 列表串状态:一个可拖拽排序的待办列表,每行带一个未提交的输入框。用数组下标当 key,拖动排序后,某行输入框里的草稿“串”到了别的行。这是什么问题?涉及哪两章的哪个概念?正确做法?
  2. S2 · 定时器不动:一个 useEffect(空依赖)里开 setInterval 想每秒给 count 加 1,count 却停在 1。从“快照”和“依赖数组”两个角度解释,并给出两种修法及取舍。
  3. S3 · 选型权衡:要做一个每秒更新几十次的实时仪表盘,团队在 React 与 Solid(signals)之间犹豫。从“重跑 + diff 的代价”出发,说出 React 的潜在劣势、以及 React 19 时代用什么抵消它。
  4. S4 · ref 还是 state:需求是“记住用户上一次悬停的卡片 id,用于下次交互判断,但这个 id 不直接显示在界面上”。该用 state 还是 ref?如果改成“要把这个 id 显示出来”,结论怎么变?
  5. S5 · RSC 切分:一个“文章正文 + 底部点赞按钮 + 评论输入框”的页面,用 Server Components 该如何划分服务端 / 客户端?划分依据是哪一章的什么概念?
亲手画一张图

合上教程,在纸上画出 §1.2 的 UI = f(state) 循环——只画 4 个节点:state → 组件 f → UI → DOM → setState,再连回 state。
画完回到图 1.2 对照三件事:① setState 的箭头有没有指回 state?② 有没有漏掉“用户交互”这条触发入口?③ 你能不能在这张图上,指出第 3 章的“快照”发生在哪个节点、第 4 章的 effect 挂在哪一步之后?画得出来,这份心智模型才算真的是你的。

§全部答案(先做完再展开)

答案集中在这里。判别层尤其要先写出自己的版本——这一层的价值全在“自己先做选择”。

展开全部答案

概念层

  1. 把“计算并执行 DOM 的差异更新”转移给了 React。你只产出目标描述,怎么把 DOM 改成那样由 React 负责。
  2. el 是一个普通 JS 对象(React 元素,形如 { type: "h1", props: { children: "Hi" } }),页面上没有出现 <h1>。它要被 React 渲染进根节点才会变成真实 DOM——写 JSX ≠ 改 DOM。
  3. 因为数据所有权在拥有它的组件,props 是传下来的只读参数;子组件改 props 不会触发任何重渲染。唯一合法途径:调用父组件传下来的回调,由父组件 setState。
  4. 不是。重渲染 = 重新调用组件函数得到新描述;只有当新旧描述 diff 出差异时,提交阶段才改对应的真实 DOM。重渲染可以完全不改 DOM。

原理层

  1. ① 类型不同则整棵子树销毁重建,不深入比较;② 同位置同类型则复用同一 DOM、只更新变化属性并递归子节点。列表再用 key 提示稳定身份。
  2. 因为 count 是本帧快照、恒为 0,三次都是 setCount(0 + 1),入队的都是“把 state 设为 1”,后者覆盖前者。要累加需用函数式更新 setCount(c => c + 1)。
  3. 因为 push 改的是数组内部,引用没变;setArr 拿到的新旧引用相同,React 用 Object.is 判定“没变”而跳过渲染(bailout)。要给新引用:setArr([...arr, x])。
  4. 因为 React 靠“第几次调用”把 Hook 对应到 fiber 上的第几个槽。放进 if,某次渲染少调一个,之后所有 Hook 的序号前移、对错槽,状态全乱。所以必须每次按相同顺序、相同数量调用。
  5. ① 下一次该 effect 因依赖变化重新运行之前(先清理旧的);② 组件卸载时(最后一次清理)。
  6. useMemo / useCallback / React.memo。不改变语义——它只自动跳过没必要的重算,state → UI 的关系不变。

应用判别层

  1. S1(涉及 §2.3 key + §3 状态绑位置):下标当 key 让 React 按位置匹配,排序后位置变、下标也变,于是旧 DOM 节点(含未提交输入草稿)被复用给了新数据。正确做法:用每行数据自带的稳定 id 当 key,让匹配按身份而非位置进行。只想到“key 要稳定”而没意识到“输入草稿是绑在 DOM 节点位置上的状态”,就漏了第 2/3 章那一半。
  2. S2(涉及 §3.1 快照 + §4.3 依赖):空依赖让 effect 只在挂载时跑一次,那次闭包永远捕获首帧 count = 0,于是每秒都 setCount(0 + 1)。修法一:函数式 setCount(c => c + 1),不读快照、读队列上一个结果,空依赖也对。修法二:把 count 写进依赖,每次变化重建 interval、捕获新快照——但会频繁重建定时器,通常不如修法一。
  3. S3(涉及 §2.1 render 代价 + §5.2 备选):React 默认重跑组件 + diff,高频更新下可能做较多“算了又 diff”的工作,这是 signals(精确更新、无需 diff 整棵树)的相对优势所在。React 19 时代的抵消手段:React Compiler 自动记忆化跳过未变子树,加上把高频区域拆细、用 ref / 非受控减少 state 驱动的重渲染。只比较“谁快”而不谈“React 用什么抵消代价”,就只答了一半。
  4. S4(涉及 §4.2 ref + §3 state):不显示、只用于下次交互判断 → 用 ref(跨渲染保留且改它不触发重渲染,避免多余渲染)。一旦要把这个 id 显示出来,就必须改用 state——ref 改了不触发渲染,界面不会更新。判据就是“它影不影响 UI”。
  5. S5(涉及 §1.2 纯函数 + §5.6 RSC):正文是纯展示、无 state,放服务端组件(直读数据、不进 bundle);点赞按钮带本地 state + 点击事件、评论输入框是受控输入,二者都需 "use client" 放客户端。划分依据:“有没有 state / effect / 事件”,即第 1 章“纯函数 vs 有交互”的延伸。