说说 React render 方法的原理?在什么时候会被触发?
一、是什么
在 React 中,render 是将组件转化为虚拟 DOM(React Element)的核心过程。它描述了"当前状态下 UI 应该长什么样",而非直接操作真实 DOM。React 通过比较前后两次 render 的结果(Reconciliation),计算出最小 DOM 变更并应用到页面上。
对于类组件,render 是 render() 方法;对于函数组件,整个函数体本身就是 render 过程。
// 类组件:render() 方法返回 React Element
class Greeting extends React.Component<{ name: string }> {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// 函数组件:函数返回值即 render 结果
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}</h1>;
}
两者都返回 React Element,它是一个描述 UI 结构的普通 JavaScript 对象:
// <h1>Hello, React</h1> 在运行时是这样的对象
{
type: 'h1',
props: { children: 'Hello, React' },
key: null,
ref: null,
}
二、render 的触发时机
2.1 初次渲染
应用首次挂载时,React 18 使用 createRoot API:
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
此时 React 会从根组件开始,递归调用所有子组件的 render,构建完整的虚拟 DOM 树,然后一次性创建真实 DOM 并挂载到页面。
2.2 setState / useState 更新
当组件调用 setState(类组件)或 useState 的 setter(函数组件)时,会标记该组件需要重新渲染:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prev => prev + 1); // 触发重新渲染
};
console.log('Counter render'); // 每次状态变化都会执行
return <button onClick={handleClick}>{count}</button>;
}
React 18 中所有的状态更新都会被自动批处理(Automatic Batching),即使在 setTimeout、Promise 或原生事件中:
function BatchExample() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
// React 18:这两次更新被合并为一次 render
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次重新渲染
}, 0);
};
return <div>{count} - {String(flag)}</div>;
}
2.3 Props 变化
当父组件重新渲染时,会重新创建子组件的 props 对象。即使 props 值没变,子组件默认也会重新渲染:
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
{/* Parent 每次 render,Child 也会 render,即使 name 没变 */}
<Child name="React" />
</div>
);
}
function Child({ name }: { name: string }) {
console.log('Child render'); // 每次父组件更新都会打印
return <span>{name}</span>;
}
2.4 forceUpdate(类组件)
类组件可以通过 this.forceUpdate() 强制跳过 shouldComponentUpdate 检查,直接触发重新渲染:
class DataDisplay extends React.Component {
externalData = externalStore.getData();
refresh = () => {
this.externalData = externalStore.getData();
this.forceUpdate(); // 跳过 shouldComponentUpdate
};
render() {
return <div>{this.externalData.value}</div>;
}
}
函数组件中可以用 useReducer 模拟类似效果:
function useForceUpdate() {
const [, forceUpdate] = useReducer(x => x + 1, 0);
return forceUpdate;
}
2.5 Context 变化
当 Context.Provider 的 value 发生变化时,所有消费该 Context 的组件都会重新渲染,无视 React.memo 等优化措施:
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext.Provider value={theme}>
<Header />
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
);
}
// 即使被 React.memo 包裹,context 变化时仍会重新渲染
const ThemedButton = React.memo(function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>按钮</button>;
});
2.6 父组件重新渲染
React 的默认行为是:父组件重新渲染时,所有子组件都会重新渲染,无论子组件的 props 是否变化。这是许多性能问题的根源,也是 React.memo、useMemo、useCallback 存在的原因。
三、React 18 createRoot 与 Legacy render
React 18 引入了新的根节点 API,以支持并发特性:
// React 18 新 API — 启用并发特性
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root')!);
root.render(<App />);
// Legacy API — 不支持并发特性
import { render } from 'react-dom';
render(<App />, document.getElementById('root'));
两者的关键区别:
四、并发渲染的影响
React 18 引入的并发渲染(Concurrent Rendering)改变了 render 的执行方式。在传统的同步模式下,一旦 render 开始就必须完成整棵树才能交出控制权。并发模式下 render 可以被中断和恢复:
import { useTransition, useState } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value); // 高优先级:立即更新输入框
startTransition(() => {
setResults(filterLargeList(value)); // 低优先级:可以被中断
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span>搜索中...</span>}
<ul>
{results.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
并发渲染让 React 可以同时准备多个版本的 UI,优先响应用户交互,将低优先级的渲染工作延后处理。
五、render 阶段 vs commit 阶段
React 的更新过程分为两个阶段:
render 阶段(可中断)
├── 调用组件函数 / render 方法
├── 生成新的虚拟 DOM
├── Diff 比较(Reconciliation)
└── 计算需要的 DOM 变更(Effect List)
↓
commit 阶段(不可中断)
├── 执行 DOM 操作(增、删、改)
├── 调用 useLayoutEffect / componentDidMount / componentDidUpdate
└── 异步调度 useEffect
render 阶段是纯计算过程,不产生副作用,因此在并发模式下可以被中断和重新开始。commit 阶段涉及真实 DOM 操作,必须同步完成以保证 UI 一致性。
六、总结
render 方法的核心职责是根据当前 state 和 props 返回 UI 描述(React Element)。它会在以下时机被触发:
- 初次挂载 —
createRoot().render() 调用
- 状态更新 —
setState 或 useState setter
- Props 变化 — 父组件传递新的 props
- forceUpdate — 类组件强制更新
- Context 变化 — Provider value 更新
- 父组件渲染 — 默认级联触发子组件渲染
React 18 的并发渲染让 render 过程变得可中断,配合自动批处理大幅减少了不必要的渲染次数。理解 render 的触发机制是进行性能优化的基础,只有知道为什么组件会重新渲染,才能有针对性地阻止不必要的渲染。