前言
在 React 中,我们常说“渲染(Render)”,但它不仅仅是将 HTML 丢给浏览器那么简单。Render 是一个包含 计算(Reconciliation) 与 提交(Commit) 的复杂过程。理解这一过程,能帮助我们写出更高性能的代码。
一、 Render 的核心三部曲
当 React 决定更新界面时,会经历以下三个关键阶段:
1. 创建虚拟 DOM (Virtual DOM)
JSX 本质上是 React.createElement() 的语法糖。Babel 会将 JSX 编译为 JS 调用,生成一个描述 UI 的对象树(即虚拟 DOM)。
结构定义参考:
// 编译后的逻辑(简化版)
const vDom = {
type: 'div',
props: {
className: 'active',
children: 'Hello'
}
};
2. Diff 算法比较 (Reconciliation)
React 并不会盲目替换整个 DOM,而是通过 Diff 算法 对比“新旧两棵虚拟 DOM 树”。
- 同层比较:只比较同一层级的节点。
- 类型检查:如果节点类型变了(如
div变p),则直接销毁重建。 - Key 值优化:通过
key属性识别节点是否只是移动了位置。
3. 渲染真实 DOM (Commit)
在计算出最小差异(Patches)后,React 的渲染器(如 react-dom)会将这些变更同步到真实浏览器环境,触发重绘与回流,使用户看到更新。
二、 触发渲染的四大时机
在函数式组件中,Render 过程可能由以下四种情况触发:
| 触发场景 | 描述 |
|---|---|
| 首次渲染 | 应用启动,将组件树完整挂载到页面上。 |
| State 改变 | 当调用 useState 的 set 函数或 useReducer 的 dispatch 时。 |
| Props 改变 | 父组件重新渲染导致传给子组件的属性发生变化。 |
| Context 改变 | 组件通过 useContext 订阅了上下文,且 Provider 的 value 发生变更。 |
三、 实战演示:观测渲染行为
我们可以通过简单的日志输出,来观察不同场景下的渲染行为。
import React, { useState, useContext, createContext } from 'react';
// 创建 Context
const AppContext = createContext(0);
// 子组件
const Child: React.FC<{ count: number }> = ({ count }) => {
console.log("子组件 Render...");
return <div>父级传入的 Props: {count}</div>;
};
// 顶层组件
const Home: React.FC = () => {
const [num, setNum] = useState<number>(0);
const [other, setOther] = useState<boolean>(false);
console.log("Home 组件 Render...");
return (
<AppContext.Provider value={num}>
<div style={{ padding: '20px' }}>
<h2>Render 触发测试</h2>
{/* 1. 修改 State 触发 */}
<button onClick={() => setNum(prev => prev + 1)}>
修改 State (Count: {num})
</button>
{/* 2. 这里的修改虽然没传给 Child,但父组件重新渲染会导致 Child 也重新渲染 */}
<button onClick={() => setOther(!other)}>
无关渲染测试: {String(other)}
</button>
{/* 3. Props 改变触发子组件渲染 */}
<Child count={num} />
</div>
</AppContext.Provider>
);
};
export default Home;