知

模块一 · 起步与渲染

第 3 / 18 节17 分钟阅读型

React 的心智模型:UI = f(state)

把 React 放回它解决的问题里:视图是状态的函数。这一节讲清声明式的三个推论、一次渲染的输入输出,以及状态与视图不同步的三种原因。

读完这一节,你能做到

  1. 用 UI = f(state) 解释 React 的渲染模型。
  2. 说出声明式带来的三个推论,以及它们对应的编码约束。
  3. 判断一次"页面没更新"是状态、渲染还是提交阶段的问题。
  4. 说清 React 与 Vue 在"更新"这件事上的思维差异。

一、UI = f(state) 到底在说什么

React 的核心假设是:给定同一份状态,渲染出的界面应该完全一致。你可以把组件理解成一个函数:

function Counter({ count }) {
  return <button>{count}</button>;
}

输入是 props 与 state,输出是"界面描述"(React 元素)。React 负责把这份描述同步到真实 DOM。

这与命令式最大的区别是:你不再决定"怎么改 DOM",只决定"状态应该是什么"。改 DOM 的时机、范围、顺序,都由 React 统一调度。

二、声明式的三个推论

推论含义对应的约束
状态是唯一来源界面上的每个值都应能追溯到 state/props不要用 DOM 或变量"偷偷"存状态
渲染必须是纯的同样的输入必须得到同样的输出渲染期间不能发请求、改全局、写 DOM
副作用要隔离渲染之外的事情放到 effect 或事件里不要在 render 里订阅、定时、打日志做业务

违反第一条,会出现"界面和状态对不上";违反第二条,会出现"渲染两次结果不同";违反第三条,会出现"重复请求、内存泄漏、难以复现的 bug"。

为什么渲染必须纯?因为 React 可能多次调用组件函数:StrictMode 双调用、并发渲染中断后重放、状态更新后重算。如果渲染里发请求,就会重复请求;如果渲染里改全局变量,结果就依赖调用次数。纯函数保证“调用几次都一样”,React 才能安全地调度。

一个常见反例:在 render 里 items.sort(),排序会修改原数组,父组件传入的 props 被改掉,下一次渲染结果就变了。正确做法是 [...items].sort(),或者提前在 state 更新时排好序。

三、一次渲染的输入与输出

输入:props、state、context
处理:执行组件函数,生成 React 元素树
输出:新的元素树,交给协调器(reconciler)

几个容易混淆的点:

  • 组件函数执行不等于 DOM 更新。render 只是"算出应该长什么样",commit 阶段才改 DOM。
  • 每次渲染都有自己的 props 与 state 快照。事件回调里读到的是"那次渲染时"的值,不是最新值。
  • React 不保证渲染次数,只保证"状态一致时结果一致"。所以不要把渲染次数当业务逻辑。

渲染快照最典型的例子:

<button onClick={() => setCount(count + 1)}>{count}</button>

连续点两次,count 只加 1。因为这个回调闭包捕获的是“这次渲染时”的 count,两次点击都在同一个闭包里执行。改成 setCount(c => c + 1),React 会把最新值传进函数,才能加 2。

另一个推论是:能从现有 state 算出来的值,不要再存一份 state。count 如果能由 items.length 得到,就写成派生值;多存一份就要维护两份数据的一致性。

四、状态与视图不同步的三种原因

现象根因排查
改了变量但页面没变改的不是 state检查是否直接改对象/数组
页面变了两次又变回去渲染有副作用或 StrictMode 双调用检查 render 里的副作用
事件里读到旧值闭包捕获了旧快照用函数式更新或 ref

判断顺序:先确认"状态有没有真的变",再确认"组件有没有重新渲染",最后确认"提交阶段有没有执行"。

五、React 与 Vue 的思维差异

维度ReactVue
更新触发调用 setState,显式请求渲染依赖变化自动触发
依赖追踪手动写依赖数组运行时自动收集
渲染粒度组件级组件级 + 编译优化
心智负担快照与闭包响应式与代理

React 把"什么时候渲染"交给你(setState),把"怎么渲染"交给框架;Vue 把"什么时候渲染"也交给框架。理解这一点,就不会把 Vue 的 watch 思维带进 React。

六、常见坑

  1. 直接修改 state 对象,引用没变,React 认为没变化。
  2. 在 render 里发请求或改状态,造成循环渲染。
  3. 把"渲染次数"当成性能问题,实际上大部分渲染很便宜。
  4. 用普通变量保存跨渲染的数据,刷新后丢失。
  5. 期望事件回调里读到最新 state,却忽略了闭包快照。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个计数器,用 useState 与普通变量各实现一次,观察差异。
  2. 在组件函数里打日志,记录每次渲染的 props 与 state。
  3. 故意在 render 里改状态,观察 React 的报错信息。

② 让 AI 写同一题

把第 1 题交给 AI,并要求它额外解释一句:为什么普通变量不行。

③ 对比要盯的点

  • 它的解释是不是停在“React 是异步的”?这句话只描述了现象,没给出原因。
  • 它有没有提到“每次渲染里的 state 是一份快照”?这是面试真正的分水岭。
  • 代码里它写的是 count++ 还是 setCount(count + 1)?如果无效,它的注释是否承认无效?

④ 一句话取舍

它答对了现象却可能讲错了机制。这种情况在面试里是加分还是扣分,取决于你能不能当场把它扶回“快照”这条线上。

八、自测

  1. UI = f(state) 的核心假设是什么?
  2. 三个推论分别对应什么编码约束?
  3. render 与 commit 的分工是什么?
  4. 事件里读到旧值的原因是什么?
  5. React 与 Vue 在更新触发上的差异是什么?

参考答案要点:同状态同界面;唯一来源/纯渲染/副作用隔离;算出元素树 vs 改 DOM;闭包快照;setState 显式 vs 依赖自动。

附:用三个问题自检心智模型

  1. 这个值能不能由现有 state 算出来?能,就不要存。
  2. 这段逻辑是不是由渲染引起?是,放 effect;否,放事件。
  3. 这次修改会不会改变引用?不会,就用新对象/新数组。

三个问题覆盖了大部分 React 反模式:派生状态、错误副作用、原地修改。每次写组件前问一遍,比事后 debug 便宜得多。

再补一个判断:如果一个组件既负责取数、又负责展示、还负责写全局状态,它就不再是“状态的函数”,而是“副作用集合”。先拆职责,再谈性能。

第三种情况最隐蔽:把 props 复制到 state,后续 props 变化时 state 不跟着变,界面就“卡”在旧值。除非你明确需要“仅初始值”语义,否则不要复制。复制 props 是最常见的反模式。模型先于 API。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解