React useCallback配合memo避免子组件无意义重渲染
在React应用开发中,父组件的状态更新往往会触发所有子组件的重渲染,即使子组件的props并没有发生变化。这种“无意义重渲染”会消耗宝贵的计算资源,导致页面卡顿。通过结合 React.memo 和 useCallback,可以精准控制组件的更新时机,提升应用性能。
1. 复现性能瓶颈场景
首先,构建 一个包含父子组件的示例,直观地感受无意义的重渲染。
创建 一个名为 ParentComponent.js 的文件。在这个组件中,定义 一个 count 状态,并实现 一个按钮来更新 该状态。同时,引入 一个子组件 ChildComponent,并向其传递 一个 onClick 回调函数。
import React, { useState } from 'react';
// 子组件定义
const ChildComponent = ({ onClick }) => {
console.log('子组件渲染了...');
return (
<div style={{ border: '1px solid blue', padding: '10px', margin: '10px' }}>
<p>我是子组件</p>
<button onClick={onClick}>子组件按钮</button>
</div>
);
};
// 父组件定义
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 直接定义的内联函数
const handleClick = () => {
console.log('按钮被点击');
};
return (
<div style={{ padding: '20px' }}>
<h1>父组件计数: {count}</h1>
<button onClick={() => setCount(count + 1)}>增加计数</button>
{/* 传递给子组件 */}
<ChildComponent onClick={handleClick} />
</div>
);
export default ParentComponent;
运行 代码并打开 浏览器控制台。每点击 一次“增加计数”按钮,你会发现控制台输出了“子组件渲染了...”。
分析 原因:
- 父组件
count状态改变,触发父组件重渲染。 - 父组件重渲染时,
handleClick函数被重新创建(在JavaScript中,function() {} !== function() {})。 - 子组件接收到的
onClickprops 引用发生了变化。 - React 认为数据变了,于是强制 子组件重渲染。
2. 初步尝试:使用 React.memo
为了阻止子组件因父组件状态变化而被动渲染,使用 React.memo 来包裹 子组件。这是一个高阶组件,它会对 props 进行浅比较。
修改 ChildComponent 的导出方式:
// 使用 React.memo 包裹
const MemoizedChildComponent = React.memo(ChildComponent);
export default MemoizedChildComponent;
保存 代码并再次测试。你会发现,点击“增加计数”按钮时,子组件依然在渲染。
探究 失败原因:
React.memo 默认通过浅比较(即 ===)来判断 props 是否变化。虽然函数的内容没变,但父组件每次渲染都会生成一个新的函数引用。对于 React 来说,onClick 的新引用不等于旧引用,因此浅比较失败,React.memo 失效。
3. 核心解决方案:引入 useCallback
要解决引用变化的问题,需要使用 useCallback Hook 来缓存 函数引用。useCallback 会在依赖项不变的情况下,返回上一次渲染中缓存的函数引用。
重构 父组件中的 handleClick 函数:
import React, { useState, useCallback } from 'react';
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 使用 useCallback 包裹函数
// 第二个参数是依赖数组,这里 handleClick 不依赖任何外部变量
const handleClick = useCallback(() => {
console.log('按钮被点击');
}, []);
return (
<div style={{ padding: '20px' }}>
<h1>父组件计数: {count}</h1>
<button onClick={() => setCount(count + 1)}>增加计数</button>
{/* 这里的 onClick 引用在 count 变化时保持不变 */}
<MemoizedChildComponent onClick={handleClick} />
</div>
);
};
刷新 页面并点击 “增加计数”按钮。此时,控制台不再输出“子组件渲染了...”。
验证 效果:
- 父组件因
count变化而重渲染。 - React 检查
useCallback的依赖数组[]。由于依赖为空且未变,useCallback返回 上一次缓存的handleClick函数引用。 MemoizedChildComponent接收到的onClickprop 与之前相同。- 浅比较通过,子组件跳过 渲染。
4. 处理依赖项:何时更新函数
在实际开发中,回调函数往往需要使用父组件的状态或 props。此时,必须将使用的变量加入 useCallback 的依赖数组中。
修改 场景:假设 handleClick 需要使用父组件的 count 变量。
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 将 count 加入依赖数组
const handleClick = useCallback(() => {
console.log(`当前计数是: ${count}`);
}, [count]);
// ...其余代码
测试 行为:
- 点击 “增加计数”按钮,
count变化。 useCallback检测 到依赖项count发生了变化。useCallback创建 一个新的函数引用(其中闭包包含了新的count值)。- 子组件接收到新的
onClick引用。 - 子组件触发 重渲染。
这是符合预期的行为,因为子组件的逻辑依赖于父组件的 count,它必须更新以获取最新数据。
5. 进阶注意事项:避免对象/数组的引用变化
即使使用了 useCallback 和 React.memo,如果传递给子组件的 props 中包含对象或数组,依然可能导致重渲染问题。
查看 常见错误代码:
const ParentComponent = () => {
// ...
// 每次渲染都会创建一个新的对象字面量
const userInfo = { name: 'Alex', age: 18 };
return (
// ...
<MemoizedChildComponent onClick={handleClick} user={userInfo} />
);
};
在这个例子中,虽然 handleClick 被缓存了,但 user 对象每次渲染都是一个新的引用,导致 React.memo 失效。
解决 方法:使用 useMemo 来缓存 对象或数组。
import { useMemo } from 'react';
const ParentComponent = () => {
// ...
// 使用 useMemo 缓存对象
const userInfo = useMemo(() => ({
name: 'Alex',
age: 18
}), []); // 空依赖数组表示对象结构永不变化
return (
// ...
<MemoizedChildComponent onClick={handleClick} user={userInfo} />
);
};
6. 策略对比与总结
为了帮助快速判断何时使用这些优化手段,请参考下表。
| 场景描述 | 父组件更新时子组件行为 | 优化方案 |
|---|---|---|
| 无优化<br>(直接传递内联函数和组件) | 总是重渲染 | 无 |
| 仅 React.memo<br>(传递内联函数) | 总是重渲染<br>(因函数引用变化) | 无效 |
| React.memo + useCallback<br>(依赖为空) | 不重渲染<br>(函数引用稳定) | 推荐<br>(用于纯UI组件交互) |
| React.memo + useCallback<br>(依赖包含状态) | 状态相关时重渲染<br>(逻辑需要最新数据) | 推荐<br>(用于依赖数据的回调) |
| 传递对象/数组 props | 即使函数缓存也会重渲染<br>(对象引用变化) | 必须配合 useMemo |
执行 性能优化的最终检查清单:
- 确认 子组件是纯展示组件或计算开销较大。
- 使用
React.memo包裹 子组件。 - 检查 父组件传递的 props,特别是函数类型。
- 对所有传递给子组件的函数,使用
useCallback进行包裹。 - 检查
useCallback的依赖数组,确保只在必要时更新函数引用。 - 如果传递了对象或数组,使用
useMemo进行包裹。

暂无评论,快来抢沙发吧!