useCallback
是 React 的一个 Hook,它用于 缓存函数,以便在组件的重新渲染中 保持函数引用不变,避免不必要的重新渲染或副作用执行。
为什么使用 useCallback
?
React 在组件渲染时,会重新创建每个在组件中定义的函数。如果这个函数作为 useEffect
或 useMemo
的依赖,或者传递给子组件,它的每次重新定义都会导致依赖的副作用重新执行或子组件重新渲染,这可能会导致性能问题,尤其是在子组件依赖这些函数时。
useCallback
可以帮助我们在依赖项没有变化的情况下 避免函数重新创建。
useCallback
的语法
const memoizedCallback = useCallback(
() => {
// Your callback function logic here
},
[dependencies] // 依赖项数组,当依赖项变化时,才会重新创建函数
);
- 第一个参数:你希望缓存的函数。
- 第二个参数:依赖项数组,当数组中的任何依赖项发生变化时,
useCallback
会返回一个新的函数,否则它会返回缓存的函数。
使用 useCallback
的常见场景
1. 优化传递给子组件的回调函数
如果一个函数被传递给子组件,并且这个子组件通过 React.memo
或 PureComponent
来优化性能(避免不必要的重新渲染),我们通常需要确保传递给子组件的函数在父组件渲染时保持引用不变。否则,子组件会因为函数引用变化而重新渲染。
import React, { useState, useCallback } from 'react';
const ChildComponent = React.memo(({ handleClick }) => {
console.log('ChildComponent rendered');
return <button onClick={handleClick}>Click me</button>;
});
const ParentComponent = () => {
const [count, setCount] = useState(0);
// 使用 useCallback 缓存 handleClick 函数
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 依赖 count,只有 count 变化时,handleClick 才会更新
return (
<div>
<ChildComponent handleClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};
export default ParentComponent;
useCallback
确保handleClick
函数在count
不变的情况下不会重新创建,从而避免了ChildComponent
的不必要重新渲染。2. 防止不必要的副作用执行
如果某个函数在 useEffect
或 useMemo
中被使用,并且这个函数被当作依赖项传递给 useEffect
,则每次函数重新创建都会导致 useEffect
被重新执行。如果你希望 useEffect
只在依赖项变化时执行,使用 useCallback
缓存函数可以防止这种情况。
import React, { useState, useEffect, useCallback } from 'react';
const MyComponent = () => {
const [count, setCount] = useState(0);
const handleInit = useCallback(() => {
console.log('Initializing...');
}, []); // handleInit 函数不会改变
useEffect(() => {
handleInit();
}, [handleInit]); // 只有 handleInit 改变时才会执行
return (
<div>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<p>Count: {count}</p>
</div>
);
};
- 在这个例子中,
handleInit
只有在组件挂载时执行一次,因为它被useCallback
缓存,且没有依赖项。
使用 useCallback
的注意事项
避免滥用:
useCallback
只是为了性能优化。不要为了避免函数重新创建而滥用它,只有在函数作为useEffect
或子组件的依赖时才需要使用。- 如果一个函数在组件内频繁变化,并且它不传递给子组件或不用于副作用中,使用
useCallback
可能没有实际效果,反而会增加代码复杂度。
与
useMemo
的区别:useMemo
用于缓存值,而useCallback
用于缓存函数。这两个 Hook 的本质相同,都是通过依赖项控制缓存。
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b]); const memoizedCallback = useCallback(() => expensiveComputation(a, b), [a, b]);
总结
useCallback
是 React 中用于优化性能的工具,特别是在函数作为子组件 props 或 useEffect
的依赖时,可以避免不必要的重新渲染或副作用执行。需要谨慎使用,只在必要时才用,避免过度优化导致代码复杂性增加。
代码 1:使用 useCallback
包裹 getPersonList
函数
const getPersonList = useCallback(async () => {
try {
const res = await apiUtil.getPersonList({
userId: userInfoStore.userInfo?.id,
pageSize,
pageNum,
});
console.log('res', res);
setPersonList(res.data);
} catch (e) {
console.log(e);
}
}, [pageSize, pageNum, userInfoStore.userInfo?.id]);
useEffect(() => {
getPersonList();
}, [getPersonList]);
代码 2:直接在 useEffect
中调用 getPersonList
const getPersonList = async () => {
try {
const res = await apiUtil.getPersonList({
userId: userInfoStore.userInfo?.id,
pageSize,
pageNum,
});
console.log('res', res);
setPersonList(res.data);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getPersonList();
}, [pageSize, pageNum, userInfoStore.userInfo?.id]);
区别
1. useCallback
和 useEffect
的依赖管理
代码 1(使用
useCallback
):getPersonList
是通过useCallback
包裹的,这意味着getPersonList
函数本身只会在其依赖项发生变化时(即pageSize
,pageNum
, 和userInfoStore.userInfo?.id
)重新创建。如果这些依赖项没有发生变化,getPersonList
会保持引用不变。useEffect
的依赖项是getPersonList
,这意味着useEffect
只有在getPersonList
的引用发生变化时才会重新执行。因为getPersonList
是通过useCallback
包裹的,所以只有在其依赖项变化时(如pageSize
,pageNum
,userInfoStore.userInfo?.id
)才会触发useEffect
。- 优势:通过
useCallback
,你确保了getPersonList
的函数在依赖项不变时不会被重新创建,从而避免了不必要的重新渲染和useEffect
执行。
代码 2(直接在
useEffect
中调用):getPersonList
函数没有被包裹在useCallback
中,每次useEffect
执行时,都会重新创建一个新的getPersonList
函数实例。useEffect
会依赖pageSize
,pageNum
, 和userInfoStore.userInfo?.id
,并且每次这些值发生变化时,都会重新执行getPersonList
函数,即使它是直接定义的。- 问题:每次组件渲染或依赖项变化时,
getPersonList
都会被重新创建,虽然它本身不会直接影响useEffect
执行,但可能会导致不必要的重新渲染或性能问题,特别是如果getPersonList
很复杂或涉及一些其他的计算。
2. 性能优化
代码 1 使用
useCallback
可以防止在每次渲染时都重新创建getPersonList
函数。useCallback
会在依赖项变化时返回同一个函数实例,从而避免了不必要的函数重建。- 优化场景:如果
getPersonList
被传递给子组件或用作事件处理程序,使用useCallback
可以避免子组件的重新渲染或事件处理程序的重新绑定。
- 优化场景:如果
代码 2 没有使用
useCallback
,每次组件渲染时都会重新定义一个新的getPersonList
函数,这在某些场景下可能导致不必要的性能开销,尤其是当组件较为复杂或频繁更新时。
3. 语义上的差异
代码 1 更加明确地表明
getPersonList
是一个“稳定的”函数,只有在其依赖项发生变化时才会重新创建,并且通过useEffect
监听其变化。这种方式更适合处理复杂的异步函数,或者当getPersonList
被传递给其他组件时(例如作为 props)。代码 2 更加简单和直观,因为它直接在
useEffect
中定义和调用getPersonList
,没有额外的包裹,适合逻辑比较简单且不需要优化的场景。
总结
- 代码 1 是更为“优化”的做法,通过
useCallback
使getPersonList
函数稳定,并且避免不必要的函数重建和useEffect
执行。 - 代码 2 更简单,但可能会在某些情况下导致性能问题,尤其是当
getPersonList
变得复杂或与其他优化需求相关时。
如果没有传递 getPersonList
到子组件或者没有复杂的依赖关系,代码 2 也可以正常工作。如果有性能需求或复杂的函数逻辑,代码 1 是更推荐的做法。