大家好,我是小杨。不知道你有没有过这种"性能焦虑"?在编写 React 函数组件时,每当看到 eslint 提示 'xxx' function makes the dependencies of useEffect Hook change on every render
,心头就一紧。
我曾经也是这样,直到我真正理解了 useCallback
的正确打开方式。今天,我们就来聊聊它,我会用最直白的方式,帮你告别无效优化,实现精准性能提升。
useCallback 到底是什么?简单饭局说清楚
你可以把 useCallback
想象成一个"记忆大师"。它的工作就是在依赖项不变的情况下,返回同一个函数引用,而不是每次渲染都创建一个新的函数。
这有什么用?我举个生活中的例子:
假设你(组件)每天都要告诉你的朋友(子组件)一个秘密(函数)。如果这个秘密每天都不一样,朋友就得每天重新记一遍(子组件重新渲染)。但如果这个秘密好几天都一样,你只需要说"还是昨天那个秘密"(相同的函数引用),朋友就不用费脑子再记了(避免不必要的渲染)。
在 React 中,useCallback
就是帮你做这个事情的。它"记住"了一个函数,只有在它的依赖项数组发生变化时,它才会"翻脸"创建一个新的。
一个真实场景:避免子组件不必要的渲染
这是 useCallback
最经典的使用场景。我们来看一段代码。
假设我有一个父组件,它包含一个昂贵的子组件 ExpensiveChildComponent
,以及一个计数器:
没有 useCallback 时的问题代码:
jsx
import React, { useState } from 'react';
// 一个假设非常"昂贵"的子组件,用了 React.memo 进行优化
const ExpensiveChild = React.memo(({ onButtonClick }) => {
console.log('子组件被渲染了!'); // 如果控制台频繁打印这个,说明性能有问题
return <button onClick={onButtonClick}>点击我</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('小杨');
// 问题在这里:每次 ParentComponent 渲染,都会创建一个全新的 handleClick 函数
const handleClick = () => {
console.log('按钮被点击了!', name); // 这里用到了 state `name`
};
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
{/* 即使子组件用了 memo,因为每次 handleClick 都是新的,子组件还是会重新渲染 */}
<ExpensiveChild onButtonClick={handleClick} />
</div>
);
}
发生了什么?
当我点击"增加计数"按钮时,count
状态改变,导致 ParentComponent
重新渲染。这会重新创建 handleClick
函数。虽然 ExpensiveChild
用了 React.memo
进行保护,但它接收的 onButtonClick
prop 每次都是一个新的函数引用,所以 React.memo
的对比失败了,子组件被迫跟着重新渲染。这完全浪费了 memo
的优化效果!
用 useCallback 进行优化:
jsx
import React, { useState, useCallback } from 'react';
const ExpensiveChild = React.memo(({ onButtonClick }) => {
console.log('子组件被渲染了!'); // 现在只有 name 改变时,这里才会打印
return <button onClick={onButtonClick}>点击我</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('小杨');
// 使用 useCallback 将函数缓存起来
// 依赖项数组 [name] 表示:只有当 name 改变时,才会重新创建 handleClick
const handleClick = useCallback(() => {
console.log('按钮被点击了!', name);
}, [name]); // ✅ 依赖项必须写全!
return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
{/* 现在,点击"增加计数"按钮时,handleClick 的引用不变,子组件不会重新渲染! */}
<ExpensiveChild onButtonClick={handleClick} />
</div>
);
}
优化后的效果:
现在,当我点击"增加计数"按钮时,虽然父组件重新渲染了,但 useCallback
发现它的依赖项 [name]
没有变化,所以它会直接返回上一次记忆的 handleClick
函数。ExpensiveChild
接收到的 onButtonClick
prop 引用没有变化,React.memo
的对比成功,阻止了子组件不必要的渲染!只有当我修改 input
(改变了 name
)时,handleClick
才会被重新创建,并触发子组件重新渲染。
useCallback 的黄金搭档:useMemo 和 React.memo
- React.memo: 用于优化子组件,对 props 进行浅比较,避免不必要的重渲染。
- useCallback : 用于稳定作为 props 传递的函数 ,辅助
React.memo
发挥作用。 - useMemo : 用于稳定作为 props 传递的复杂计算值 (比如计算数组、对象),同样是辅助
React.memo
。
它们三个常常一起使用,构成性能优化的"铁三角"。
什么时候不该用 useCallback?
别急着到处用!滥用 useCallback
反而会增加性能开销(函数本身需要被记忆,依赖项要进行比较)。遵循这个原则:
只在以下情况使用它:
- 函数被作为 prop 传递给被
React.memo
优化的子组件。 - 函数是其他 Hook(如
useEffect
)的依赖项。 - 函数被用于上下文(Context)或其他内部需要稳定引出的地方。
如果你的函数只是在一个普通的、没有性能优化需求的组件内部使用,直接定义它就好了,完全不需要 useCallback
。
总结
useCallback
不是银弹,它是一把精准的手术刀。它的核心价值在于通过稳定函数引用,来保证依赖此函数的其他优化(如 React.memo
, useEffect
)能够生效。
记住我们的优化路径:发现性能问题 -> 用 React.memo
包裹子组件 -> 再用 useCallback
和 useMemo
稳定那些会导致 memo
失效的 props。
希望这篇文章能帮你打消对 useCallback
的困惑,从此告别盲目优化,走向精准高效!如果你有其他有趣的使用场景或问题,欢迎在评论区一起讨论。
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!