React-性能优化:useCallback

43 阅读3分钟

前言

在 React 性能优化的工具箱里,useCallback 往往与 React.memo 成对出现。它的存在不是为了让函数运行得更快,而是为了保持函数引用的稳定性,从而避免下游组件不必要的重渲染。

一、 核心概念:为什么要缓存函数?

在 React 函数组件中,每次渲染都会重新执行函数体。这意味着在组件内定义的任何匿名函数,在每次渲染时都会生成一个全新的实例(引用地址不同)

  • 痛点:如果这个函数被作为 Prop 传递给一个被 React.memo 包装的子组件,子组件会因为“Props 变了(引用的函数变了)”而被迫重渲染,导致性能损耗。
  • 对策useCallback 会在内存中“锁住”这个函数实例,除非依赖项发生变化。

二、 语法拆解

  • callbackFun :要缓存的函数
  • fucncallbackFun 函数执行内容
  • [a,b]:依赖项
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

依赖数组的策略

  1. 不传数组:每次渲染都重新创建,等同于不使用 Hook。
  2. 空数组 [] :仅在组件首次挂载时创建一次,整个生命周期引用不变。
  3. 非空数组 [a, b] :只有当 ab 的值发生变化时,才会生成新的函数实例。

三、 实战场景

1. 配合 React.memo 防止子组件重渲染

这是 useCallback 最经典的使用场景。

import React, { useState, useCallback, memo } from 'react';

// 子组件:只有 Props 变化才会重渲染
const ChildButton = memo<{ onClick: () => void }>(({ onClick }) => {
  console.log("子组件重渲染...");
  return <button onClick={onClick}>增加计数</button>;
});

const Home: React.FC = () => {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // ✅ 使用 useCallback 保持引用稳定
  // 即使 text 改变触发 Home 渲染,handleClick 引用也不变,子组件就不会重渲染
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="输入不影响子组件" />
      <ChildButton onClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
};

export default Home;

2. 作为其他 Hook 的依赖(如 useEffect)

如果一个函数被用作 useEffect 的依赖,不稳定的函数引用会触发副作用无限循环。

const DataFetcher: React.FC<{ query: string }> = ({ query }) => {
  // ✅ 保持 fetchData 稳定
  const fetchData = useCallback(() => {
    console.log("根据查询条件获取数据:", query);
  }, [query]);

  useEffect(() => {
    fetchData();
  }, [fetchData]); // 只有当 query 变化导致 fetchData 变化时,才会触发

  return <div>数据查询中...</div>;
};

四、 核心警示:闭包陷阱与依赖项

“缓存”是一把双刃剑。如果依赖项声明不当,会导致函数内部产生“过期”的闭包。

1. 错误示范:闭包陷阱

const [count, setCount] = useState(0);

// ❌ 错误:依赖数组为空,函数内部却使用了 count
const increment = useCallback(() => {
  setCount(count + 1); // 这里的 count 被永远锁在了 0
}, []); 

2. 解决方案

  • 方案 A:将变量加入依赖数组 [count](这会导致函数引用在每次 count 变化时都更新)。
  • 方案 B:使用 函数式更新(推荐),这样既能拿到最新值,又不需要把变量加进依赖,保持引用稳定。
// ✅ 最佳实践:既保证了引用稳定,又能获取最新状态
const increment = useCallback(() => {
  setCount(prevCount => prevCount + 1);
}, []); // 依赖数组可以保持为空

五、 总结:何时使用 useCallback?

  1. 子组件被 memo 包裹,且接收该函数作为 Prop。
  2. 函数被作为其他 Hook 的依赖
  3. 不要过度使用:如果只是普通的按钮点击,且没有子组件性能压力,直接定义匿名函数更简洁。缓存本身也是有开销的。