为何使用useCallback后子组件仍会重渲染?useCallback包裹的函数何时会在组件不重渲染时改变?
首先,咱们先拆解你代码里的核心问题:你虽然用了useCallback包裹回调,但子组件Counter依然重渲染的原因,并不是useCallback没起作用,而是你忽略了子组件接收的另一个props——value。
当Root组件里的count或times更新时,Root本身会触发重渲染。这时候,即使handleCount/handleTimes的引用因为useCallback保持不变,但你传给Counter的value(count或times)已经发生了变化。React对比组件props时,只要有任何一个props变化,子组件就会重渲染——所以这里的重渲染是因为value更新,和回调函数的引用没关系。
先搞清楚useCallback到底有什么用
useCallback的核心作用是缓存函数的引用,避免组件每次重渲染时都创建一个全新的函数实例。它的价值主要体现在两个场景:
- 当这个函数作为props传给一个被
React.memo包裹的子组件时,可以避免因为函数引用变化导致的不必要重渲染(前提是子组件的其他props也没有变化)。 - 当这个函数被用作其他hooks(比如
useEffect、useMemo)的依赖时,能保证依赖的稳定性,避免不必要的hook触发。
另外,你代码里的useCallback写法其实存在闭包陷阱:
const handleCount = useCallback(() => { setCount(count * 2); }, []);
这里依赖数组是空的,意味着handleCount会一直是组件初始渲染时创建的那个函数,它内部的count是闭包捕获的初始值(也就是1)。所以当你点击按钮时,setCount(count * 2)其实永远是setCount(1*2),第二次点击还是设置为2,根本不会更新到4、8。
正确的写法有两种:
- 把
count加入依赖数组:
const handleCount = useCallback(() => { setCount(count * 2); }, [count]);
- 用函数式更新来避免依赖
count:
const handleCount = useCallback(() => { setCount(prev => prev * 2); }, []);
回答你的第二个问题:useCallback包裹的函数何时会在组件不发生重渲染的情况下产生变化?
答案是:永远不会。
useCallback的缓存逻辑是和组件的渲染周期绑定的——只有当组件重渲染,并且依赖数组里的依赖发生变化时,useCallback才会返回一个新的函数实例。如果组件没有重渲染,useCallback根本不会执行,自然也不会产生新的函数引用。
补充一点:如果你的组件用了useReducer,reducer函数本身是稳定的,但这和useCallback无关;另外,一些特殊的自定义hook可能会在组件不重渲染时返回新的函数,但这不是useCallback的原生行为,是自定义hook内部逻辑导致的。
最后总结
- 你当前子组件重渲染是因为
valueprops变化,和useCallback无关; useCallback用来缓存函数引用,配合React.memo才能避免子组件因函数props变化导致的重渲染;- 只有组件重渲染且依赖数组变化时,
useCallback才会返回新的函数,组件不重渲染时它不会产生变化。
内容的提问来源于stack exchange,提问作者刘亚雄

