1. 为什么需要 useCallback

在 JavaScript 中,函数是引用类型,这意味着每次创建函数时都会生成新的内存引用。React 函数组件在每次渲染时,内部定义的函数都会重新创建,导致:

  • 引用变化:即使函数内容相同,每次渲染都会生成新引用
  • 子组件不必要渲染:如果函数作为 props 传递给优化过的子组件(使用 React.memo),会触发重新渲染
  • useEffect 重复执行:如果函数是 useEffect 的依赖项,会导致副作用重复触发

引用示例 1:函数引用对比

// 示例1:相同函数,不同引用
const func1 = () => console.log("Hello");
const func2 = () => console.log("Hello");

console.log(func1 === func2); // false - 不同引用
// 示例2:相同引用
const func = () => console.log("Hello");
const func3 = func;
const func4 = func;

console.log(func3 === func4); // true - 相同引用

2. useCallback 的基本用法

useCallback 通过缓存函数引用来优化性能:

语法解析

const memoizedFn = useCallback(
  () => { /* 函数体 */ },
  [dependencies] // 依赖项数组
);

引用示例 2:useCallback 保持引用稳定

function App() {
  const [count, setCount] = useState(0);
  
  // 普通函数 - 每次渲染新引用
  const normalFn = () => console.log(count);
  
  // useCallback 缓存函数 - 引用稳定
  const memoizedFn = useCallback(() => {
    console.log(count);
  }, [count]); // 依赖 count

  console.log("普通函数引用对比:", normalFn === normalFn); // false
  console.log("memoized函数引用对比:", memoizedFn === memoizedFn); // true

  return <Child fn={memoizedFn} />;
}

3. 依赖项深度解析

依赖项决定何时创建新函数引用:

引用示例 3:依赖项对引用的影响

function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");
  
  // 依赖 count,只有 count 变化时才会创建新引用
  const countFn = useCallback(() => {
    console.log(count);
  }, [count]);
  
  // 依赖 text,只有 text 变化时才会创建新引用
  const textFn = useCallback(() => {
    console.log(text);
  }, [text]);
  
  // 空依赖,永远保持相同引用
  const stableFn = useCallback(() => {
    console.log("Stable");
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Change count</button>
      <button onClick={() => setText(t => t + "a")}>Change text</button>
    </>
  );
}

4. 结合 React.memo 的优化实践

React.memo 通过浅比较 props 来避免不必要的渲染:

引用示例 4:引用变化对 React.memo 的影响

// 优化后的子组件
const MemoChild = React.memo(({ onClick }) => {
  console.log("MemoChild rendered");
  return <button onClick={onClick}>Click</button>;
});

// 普通子组件
const NormalChild = ({ onClick }) => {
  console.log("NormalChild rendered");
  return <button onClick={onClick}>Click</button>;
};

function Parent() {
  const [count, setCount] = useState(0);
  
  // 未优化的函数 - 每次渲染新引用
  const normalFn = () => console.log("Normal click");
  
  // 优化的函数 - 引用稳定
  const memoizedFn = useCallback(() => {
    console.log("Memoized click");
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Render Parent ({count})</button>
      <NormalChild onClick={normalFn} />
      <MemoChild onClick={memoizedFn} />
    </div>
  );
}

5. 性能优化对比实验

通过实际测量展示优化效果:

引用示例 5:性能对比测试

function HeavyComponent({ value }) {
  // 模拟耗时渲染
  const start = performance.now();
  while (performance.now() - start < 2) {}
  return <div>{value}</div>;
}

function TestApp() {
  const [count, setCount] = useState(0);
  
  // 未优化的处理函数
  const unoptimizedHandler = () => {
    console.log("Unoptimized handler");
  };
  
  // 优化的处理函数
  const optimizedHandler = useCallback(() => {
    console.log("Optimized handler");
  }, []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Re-render ({count})</button>
      
      {/* 每次都会重新渲染 */}
      <HeavyComponent value="未优化" onClick={unoptimizedHandler} />
      
      {/* 使用 React.memo + useCallback 优化 */}
      <React.memo(HeavyComponent) value="已优化" onClick={optimizedHandler} />
    </div>
  );
}

6. 常见误区与最佳实践

误区 1:滥用 useCallback

// 不好的写法:简单函数不需要 useCallback
const badExample = useCallback(() => {
  setState(prev => !prev);
}, []);

// 好的写法:直接使用箭头函数
const goodExample = () => setState(prev => !prev);

误区 2:依赖项不完整

function App() {
  const [count, setCount] = useState(0);
  
  // 错误的写法:缺少 count 依赖
  const badCallback = useCallback(() => {
    console.log(count); // 闭包问题,总是打印初始值
  }, []);
  
  // 正确的写法
  const goodCallback = useCallback(() => {
    console.log(count); // 总是最新值
  }, [count]);
  
  return <button onClick={() => setCount(c => c + 1)}>Increment</button>;
}

7. 总结与决策流程图

何时使用 useCallback?

需要优化性能吗?
函数作为props传递?
直接使用普通函数
子组件使用React.memo?
使用useCallback
确保依赖项完整
优化场景 推荐方案 引用变化规则
简单函数 直接定义 每次渲染新引用
传递给优化组件的函数 useCallback 依赖变化时新引用
复杂计算值 useMemo 依赖变化时新引用
事件处理函数 视情况使用useCallback 按需控制引用

8. 扩展阅读

  1. React官方文档 - useCallback
  2. useCallback和useMemo的深入解析
  3. JavaScript引用类型详解
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐