React UI 渲染的基本原理
React 的 UI 渲染基于虚拟 DOM(Virtual DOM)机制。当组件的状态(state)或属性(props)发生变化时,React 会重新计算虚拟 DOM 树,并通过 Diff 算法对比新旧虚拟 DOM 的差异,最终将变化的部分高效更新到真实 DOM 上。
触发重新渲染的条件
- 组件状态(state)变化 :调用
setState或useState的更新函数时,组件会重新渲染。 - 父组件重新渲染:父组件渲染会导致其所有子组件默认重新渲染。
- 属性(props)变化:父组件传递的 props 更新时,子组件会重新渲染。
- Context 变化:组件订阅的 Context 值发生变化时,会触发重新渲染。
- 强制更新 :调用
forceUpdate方法(不推荐使用)。
理解React UI 渲染行为
1. React重新渲染的范围:组件级别的重新渲染
React的重新渲染是以组件为单位的,而不是整个页面:
javascript
function ParentComponent() {
const [count, setCount] = useState(0);
// 当count变化时,只有ParentComponent会重新渲染
// 但会影响其子组件
return (
<div>
<ChildComponent1 /> {/* 会重新渲染 */}
<ChildComponent2 /> {/* 会重新渲染 */}
</div>
);
}
function SiblingComponent() {
// 这个组件不会因为ParentComponent的状态变化而重新渲染
return <div>我不会重新渲染</div>;
}
2. 重新渲染时发生什么
当组件重新渲染时:
javascript
function MyComponent() {
const [count, setCount] = useState(0);
// ✅ 每次渲染都会重新执行
const expensiveValue = useMemo(() => {
console.log('计算expensive value');
return count * 1000;
}, [count]);
// ✅ 每次渲染都会重新创建
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// ❌ 每次渲染都会重新创建(性能问题)
const handleClickBad = () => {
setCount(c => c + 1);
};
// ✅ 每次渲染都会重新执行函数体
console.log('组件重新渲染了');
return (
<div>
<p>{count}</p>
<button onClick={handleClick}>增加</button>
</div>
);
}
3. 如何避免不必要的重新渲染
javascript
// 使用React.memo避免子组件不必要的重新渲染
const ChildComponent = React.memo(({ name }) => {
console.log('ChildComponent渲染');
return <div>{name}</div>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name] = useState('张三');
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
{/* name没有变化,ChildComponent不会重新渲染 */}
<ChildComponent name={name} />
</div>
);
}
//或者可以参考下面的优化策略:
function OptimizedComponent() {
const [users, setUsers] = useState([]);
const [filter, setFilter] = useState('');
// ✅ 使用useMemo缓存计算结果
const filteredUsers = useMemo(() => {
console.log('过滤用户列表');
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
// ✅ 使用useCallback缓存函数
const handleFilterChange = useCallback((e) => {
setFilter(e.target.value);
}, []);
return (
<div>
<input onChange={handleFilterChange} value={filter} />
<UserList users={filteredUsers} />
</div>
);
}
避免重复渲染的方法
使用 React.memo 优化子组件
React.memo 是一个高阶组件,用于对函数组件进行浅比较,避免不必要的渲染。仅当 props 变化时才会重新渲染。
jsx
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>;
});
合理使用 useCallback 和 useMemo
useCallback 缓存函数,useMemo 缓存计算结果,避免因函数或值重新生成导致的子组件渲染。
jsx
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
避免在渲染函数中直接创建对象或函数
在渲染函数中直接创建对象或函数会导致每次渲染时生成新的引用,可能触发子组件不必要的渲染。
jsx
// 避免这样写
function BadExample() {
return <ChildComponent style={{ color: 'red' }} onClick={() => {}} />;
}
使用 shouldComponentUpdate 或 PureComponent
类组件可以通过 shouldComponentUpdate 或继承 PureComponent 实现浅比较,减少渲染次数。
jsx
class MyComponent extends React.PureComponent {
render() {
return <div>{this.props.value}</div>;
}
}
合理拆分组件
将频繁变化的部分拆分为独立组件,减少其他部分的渲染影响。例如,将表单输入框拆分为单独组件。
使用 key 属性优化列表渲染
为列表项添加唯一的 key 属性,帮助 React 识别哪些项需要更新,避免整个列表重新渲染。
jsx
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
避免滥用 Context
Context 的变化会触发所有订阅组件的渲染,可以通过拆分 Context 或使用 useMemo 优化。
jsx
const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
调试重复渲染的工具
- React DevTools:通过高亮更新功能检查不必要的渲染。
- why-did-you-render:库用于检测潜在的不必要渲染原因。
- 性能分析工具:使用 Chrome DevTools 的 Performance 或 React Profiler 分析渲染性能。
总结
- React重新渲染是组件级别的,不是整个页面
- 当组件重新渲染时,会重新执行组件函数体内的所有代码
- 子组件会跟着父组件一起重新渲染(除非使用优化手段)
- 可以通过
React.memo、useMemo、useCallback等来优化性能 - 兄弟组件之间的状态变化不会相互影响