Dark Dwarf Blog background

React 回调函数

React 回调函数

1. useCallback 的引入

在理解 useCallbackReact.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.memouseCallback 正是为了解决这些问题而出现的。

2. React.memo: 记忆组件

a.a. 基本概念

React.memo 是一个高阶组件 (Higher-Order Component, HOC),它接收一个组件作为参数,并返回一个经过优化的新组件。

React.memo 会“记住”一个组件的渲染结果。在下一次渲染时,如果新的 props 与上一次的 props 相同,React 将跳过该组件的重新渲染,直接复用上一次的渲染结果。

const MyComponent = (props) => {
  /* 渲染逻辑 */
};

// MyMemoizedComponent 只有在 props 改变时才会重新渲染
const MyMemoizedComponent = React.memo(MyComponent);

b.b. 比较机制

React.memo 判断 props 是否相同的默认方式是浅层比较 (Shallow Comparison)。它会遍历 props 对象的所有键,并使用 Object.is 来比较新旧 props 中每个键的值。

  • 对于原始类型 (string, number, boolean, null, undefined): 浅层比较会检查值是否相等。'hello' 等于 'hello'
  • 对于引用类型 (object, array, function): 浅层比较只检查引用地址是否相同,而不会深入比较其内部的内容。

这就是问题的关键所在!在前面的例子中,父组件每次渲染都会创建新的 handleClick 函数和 userInfo 对象,导致它们的引用地址永远是新的。因此,即使 ChildComponentReact.memo 包裹,浅层比较的结果依然是 false,优化完全失效。

3. useCallback: 记忆函数

a.a. 基本概念

useCallback 是一个 React Hook,它允许我们在多次渲染之间“记住”一个函数定义,从而稳定其引用地址

useCallback 会返回该函数的一个记忆化 (memoized) 版本,这个版本只有在它的某个依赖项发生变化时才会更新。

import { useCallback } from 'react';

const memoizedCallback = useCallback(
  () => {
    // 你想要记忆的函数逻辑
    doSomething(a, b);
  },
  [a, b], // 依赖项数组
);

b.b. useCallbackReact.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. useCallbackuseMemo 的区别

useCallbackuseMemo 非常相似,但侧重点不同。

  • useCallback(fn, deps) 记忆的是函数本身。它返回一个函数
  • useMemo(() => value, deps) 记忆的是函数的返回值。它返回一个

一个简单的记忆法则:

  • useCallback: 缓存一个函数,通常用于传递给子组件。
  • useMemo: 缓存一个计算结果对象,用于避免昂贵的计算或稳定对象引用。

对于非函数类型的对象(如上文的 userInfo),我们应该使用 useMemo 来稳定其引用:

const userInfo = useMemo(() => ({
  name: 'guest',
  age: 25,
}), []); // 空依赖,对象只创建一次