React 回调函数
1. useCallback
的引入
在理解 useCallback
和 React.memo
之前,必须先理解 React 的一个核心渲染机制和由此带来的性能问题。
在 JavaScript 中,函数和对象都是引用类型。当一个 React 父组件重新渲染时,它内部定义的所有函数和对象都会被重新创建,得到一个全新的引用地址。
function ParentComponent() {
const [count, setCount] = useState(0);
// 每次 ParentComponent 渲染时,handleClick 都是一个全新的函数
const handleClick = () => {
console.log('Button clicked!');
};
// userInfo 也是一个全新的对象
const userInfo = { name: 'guest' };
return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* 将新的函数和对象作为 props 传递下去 */}
<ChildComponent onClick={handleClick} user={userInfo} />
</div>
);
}
即使 handleClick
的代码和 userInfo
的内容完全没变,但它们在内存中的引用地址在每次渲染时都是不同的。如果 ChildComponent
是一个昂贵的组件,父组件的任何状态变化都会导致它不必要地重新渲染,从而造成性能浪费。
React.memo
和 useCallback
正是为了解决这些问题而出现的。
2. React.memo
: 记忆组件
基本概念
React.memo
是一个高阶组件 (Higher-Order Component, HOC),它接收一个组件作为参数,并返回一个经过优化的新组件。
React.memo
会“记住”一个组件的渲染结果。在下一次渲染时,如果新的 props
与上一次的 props
相同,React 将跳过该组件的重新渲染,直接复用上一次的渲染结果。
const MyComponent = (props) => {
/* 渲染逻辑 */
};
// MyMemoizedComponent 只有在 props 改变时才会重新渲染
const MyMemoizedComponent = React.memo(MyComponent);
比较机制
React.memo
判断 props
是否相同的默认方式是浅层比较 (Shallow Comparison)。它会遍历 props
对象的所有键,并使用 Object.is
来比较新旧 props
中每个键的值。
- 对于原始类型 (string, number, boolean, null, undefined): 浅层比较会检查值是否相等。
'hello'
等于'hello'
。 - 对于引用类型 (object, array, function): 浅层比较只检查引用地址是否相同,而不会深入比较其内部的内容。
这就是问题的关键所在!在前面的例子中,父组件每次渲染都会创建新的 handleClick
函数和 userInfo
对象,导致它们的引用地址永远是新的。因此,即使 ChildComponent
被 React.memo
包裹,浅层比较的结果依然是 false
,优化完全失效。
3. useCallback
: 记忆函数
基本概念
useCallback
是一个 React Hook,它允许我们在多次渲染之间“记住”一个函数定义,从而稳定其引用地址。
useCallback
会返回该函数的一个记忆化 (memoized) 版本,这个版本只有在它的某个依赖项发生变化时才会更新。
import { useCallback } from 'react';
const memoizedCallback = useCallback(
() => {
// 你想要记忆的函数逻辑
doSomething(a, b);
},
[a, b], // 依赖项数组
);
useCallback
与 React.memo
的协同工作
现在,我们可以使用 useCallback
来解决 React.memo
优化失效的问题。
// 1. 一个被 React.memo 优化的子组件
const MemoizedChild = React.memo(function Child({ onClick }) {
console.log('Child is re-rendering!');
return <button onClick={onClick}>Click me</button>;
});
// 2. 父组件
function Parent() {
const [count, setCount] = useState(0);
// 使用 useCallback 记忆函数,稳定其引用
const handleClick = useCallback(() => {
console.log('Clicked!');
}, []); // 空依赖数组,函数只创建一次
return (
<div>
<p>Parent Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Parent</button>
{/*
现在 handleClick 的引用是稳定的。
当 Parent 重新渲染时,MemoizedChild 的 props.onClick 没有改变,
React.memo 的浅层比较结果为 true,从而成功跳过渲染。
*/}
<MemoizedChild onClick={handleClick} />
</div>
);
}
4. useCallback
和 useMemo
的区别
useCallback
和 useMemo
非常相似,但侧重点不同。
useCallback(fn, deps)
记忆的是函数本身。它返回一个函数。useMemo(() => value, deps)
记忆的是函数的返回值。它返回一个值。
一个简单的记忆法则:
useCallback
: 缓存一个函数,通常用于传递给子组件。useMemo
: 缓存一个计算结果或对象,用于避免昂贵的计算或稳定对象引用。
对于非函数类型的对象(如上文的 userInfo
),我们应该使用 useMemo
来稳定其引用:
const userInfo = useMemo(() => ({
name: 'guest',
age: 25,
}), []); // 空依赖,对象只创建一次