渲染和提交:触发、渲染计算与 DOM 提交三步曲
许多初学者常常把“React 渲染”与“把画面画在浏览器上”混为一谈。事实上,在你的组件向用户展示最新内容之前,需要经历一套精密的流水线处理。
理解这套流程的最佳方式,是把它想象成 在一家高级餐厅就餐的三个步骤:
- 触发点单(Triggering):把顾客的菜单送到后厨;
- 在后厨烹饪制作(Rendering):厨师根据菜单调配食材并制作菜品;
- 端菜上桌提交(Committing):服务员把做好的精美菜肴稳稳摆在顾客面前。
让我们来看看 React 内部的这三步曲。
步骤一:触发一次渲染(Triggering a render)
有且仅有两种原因会引发 React 启动渲染:
1. 组件的初次渲染(Initial Render)
当你的网页刚刚打开,React 必须把所有的组件从无到有挂载在屏幕上。这相当于顾客刚刚走进餐厅,坐下并完成了第一份点单。在底层,这通常由 createRoot(...).render(<App />) 发起。
2. 状态改变后的重新渲染(Re-render on state update)
当组件已经在屏幕上展示后,用户进行了某些操作(如点击按钮、输入表单),你调用了 set 函数(例如 setCount(count + 1))。
调用 set 函数并不是立刻就地修改 DOM,而是像顾客按下了呼叫铃:“服务员,我要加菜!”,向 React 排队登记了一次重新渲染的请求。
步骤二:React 渲染你的组件(Rendering your components)
“渲染(Rendering)”在 React 的语境里有一个非常精准的定义: 渲染就是 React 正在调用你的组件函数!
- 在初次渲染时:React 会调用根组件函数,并沿着层级向下依次调用所有子组件;
- 在后续重新渲染时:React 会调用那个触发了 state 更新的组件函数。如果该组件返回了其他子组件,React 还会继续递归调用这些子组件,以计算出在新的状态下,界面到底应该长成什么样。
核心原则:渲染过程必须是纯粹的计算! 正如我们在纯函数那一关所学的:厨师在后厨做菜时,绝对不能跑到大堂偷吃客人的面包。组件在渲染计算 JSX 的过程中,不能修改任何外部变量,不能直接操作 DOM 节点,它只负责计算并返回最新的 JSX 描述树!
步骤三:React 把更改提交到 DOM(Committing to the DOM)
在渲染完成(即组件函数全部执行完毕,得到了最新的 JSX 虚拟描述)后,React 进入“提交(Commit)”阶段:
- 初次渲染:React 使用浏览器原生 DOM API(如
appendChild())把所有生成的真实 DOM 节点直接插入到 HTML 页面中; - 重新渲染:这是 React 最具魔法的地方! React 会把刚刚计算出的最新 JSX 树,与上一次渲染的旧 JSX 树进行精密的逐项比对(Diffing)。 React 只会对发生了实际变化的 DOM 节点进行最小化修改!
极具说服力的现场实验:
观察下面这个每秒钟更新一次时钟的组件:
export default function Clock({ time }) {
return (
<>
<h1>当前时间: {time}</h1>
<input placeholder="在这里输入文字测试..." />
</>
);
}
每过一秒钟,time 状态更新,React 就会重新调用 Clock 函数。
但是,如果你在下方的 <input> 输入框里正在敲字,你会惊奇地发现:
即使每秒钟都在重新渲染,你的输入法、打到一半的拼音、光标位置丝毫没有受到任何影响!
为什么?因为 React 比对后发现,<input> 标签的属性和上一次相比没有任何变化!于是在提交阶段,React 完全不去触碰真实的 input DOM 节点,只精准更新了 <h1> 里的时间文本!
尾声:浏览器的屏幕绘制(Browser Paint)
当 React 完成 DOM 提交后,浏览器的底层排版引擎(渲染引擎)就会介入,重新计算界面的几何尺寸、样式重排,并把像素真正绘制在显示器屏幕上。这一过程被称为浏览器的 重绘(Paint / Repaint)。
总结
记住这三步流水线:
- 触发(Trigger):初次挂载或调用 set 函数排队登记点单;
- 渲染(Render):React 调用你的组件函数,在后厨计算出最新的 JSX 结构树;
- 提交(Commit):React 找出新旧差异,仅对发生变动的真实 DOM 执行最小化手术级更新;
- 随后,浏览器将最终结果绘制到屏幕上。