“为什么我点了个按钮,结果旁边的组件也跟着重新渲染了一下?”
“我不改它,它自己为什么刷新?”
React 初学者看到组件莫名其妙刷新,内心常常写满两个字:离谱。
没错,函数组件默认会在父组件更新时自动重新执行,即使它的 props 根本没变。
这篇文章,我们就来好好唠唠 React 性能优化中的两个猛将:React.memo 和 useCallback,让组件该刷就刷,不该刷死都别动。
一、React.memo:组件的记忆力增强药
我们先从 React.memo 讲起,它的任务很简单:记住上一次的 props,如果没变,就别重新渲染。
1. 默认行为回顾
来看个例子:
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<Child />
</>
);
}
function Child() {
console.log('Child 组件渲染了');
return <div>我是子组件</div>;
}
你每点一次按钮,Child 都会重新执行。尽管它的 props 没有变化,但它还是被动刷新了。这就好像你喊了个室友起床,结果把全宿舍都吵醒了,冤不冤?
2. React.memo 的介入
const Child = React.memo(() => {
console.log('Child 组件渲染了');
return <div>我是子组件</div>;
});
加上 React.memo 后,只要 Child 的 props 没变,它就不会重新执行。
React.memo 的原理其实很简单:对 props 做浅比较,能复用就复用。
if (shallowEqual(prevProps, nextProps)) {
skipRender();
} else {
rerender();
}
你可以理解为:这是一个“只在必要时重渲染”的高阶组件。
二、useCallback:函数稳定器,别每次都造新函数
1. 问题场景:函数也是 props
继续看一个例子:
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('Button clicked');
};
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<Button onClick={handleClick} />
</>
);
}
const Button = React.memo(({ onClick }) => {
console.log('Button 渲染了');
return <button onClick={onClick}>点我</button>;
});
你以为 Button 的 onClick 没变,结果还是每次都触发重渲染。为啥?因为函数是引用类型:
const handleClick = () => {}
这句代码每次执行都会返回一个新的函数对象,地址变了,React.memo 检测到 props 变了,自然就渲染了。
2. 引入 useCallback
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
useCallback 的作用是:缓存函数的引用地址,只有当依赖数组变化时才会重新生成。
你可以理解为: “用 useCallback 包过的函数,不改就别动” 。
三、React.memo + useCallback:拆不散的性能搭档
const Button = React.memo(({ onClick }) => {
console.log('Button 渲染了');
return <button onClick={onClick}>点我</button>;
});
function App() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);
return (
<>
<button onClick={() => setCount(count + 1)}>+1</button>
<Button onClick={handleClick} />
</>
);
}
在这个组合拳中:
Button使用React.memo,只有 props 变化才渲染;handleClick使用useCallback,保持引用不变;- 所以
Button就不会重复刷新了。
四、useCallback 底层原理
你可能听说过一句话:
useCallback(fn, deps) 的本质就是 useMemo(() => fn, deps)
这是真的。它就是对函数的引用做了一层缓存,避免每次重新创建函数。
但它和 useMemo 有一个重要区别:
| Hook | 缓存的是什么 |
|---|---|
useMemo | 计算结果 |
useCallback | 函数本身 |
五、你以为安全,实际错用的场景
1. 缓存了旧数据
const handleClick = useCallback(() => {
console.log(count);
}, []);
这个写法看似没有问题,实际上它会永远打印初始值 0,因为 count 没在依赖里,函数不会重新生成。
修正:
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
要想用 useCallback,一定要搞清楚:它“记住”的是你定义函数的那一刻的上下文环境。
六、什么时候该用 memo 和 useCallback?
别盲用。它们虽然听起来“高性能”,但也会消耗内存来做缓存和比较。
使用建议:
- 子组件是函数组件
- 子组件渲染频率高
- 子组件 props 不常变
- 子组件自身逻辑重 / 包含 expensive 运算
不用的场景:
- 页面组件(只会初始化一次)
- 简单组件、没有性能问题的部分
- 父组件几乎不更新的地方
七、总结一波
| 工具 | 干啥用的 | 缓存内容 | 推荐搭配 |
|---|---|---|---|
| React.memo | 缓存组件 | props | useCallback / useMemo |
| useCallback | 缓存函数引用 | 函数本身 | React.memo |
| useMemo | 缓存计算值 | 函数返回值 | 高开销计算场景 |
八、彩蛋:React 到底怎么判断 props 有没有变?
React.memo 使用的是 Object.is(prevProps, nextProps) 做的浅层对比,你只要传的 props 是新对象(如函数、数组、对象),就会触发更新。
<Button config={{ color: 'red' }} />
这个写法,即使每次 config 内容不变,也会触发重渲染。解决方案:
const config = useMemo(() => ({ color: 'red' }), []);
<Button config={config} />
保持引用稳定,才能让 memo 发挥作用。
九、写在最后
React 的渲染机制本质上是“看起来很勤快,但有点过度热情”。我们要做的不是“强行制止它”,而是用更聪明的方式告诉它:
“别担心,我没变,不用你操心了。”
理解 React.memo 和 useCallback 是写好 React 性能优化的第一步。
剩下的,就是你在项目里多用、多试、多踩坑,再回来看看这篇文章。