Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

hook:useCallback

  • 用来记忆并缓存函数,在指定依赖没有更改时,直接返回缓存的函数,无需重新创建,可用于优化性能
  • 核心: useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

场景代码示例

  • 【场景 1】每次渲染都创建新函数(不建议使用):

    • 每次组件更新后,都会创建新的函数,失去了 useCallback 的意义

      // 场景:回调函数 - 每次渲染都创建新函数(无意义)
      const memoizedCallback = useCallback(() => {
        doSomething(a, b);
      });
      // 这里没有依赖项
      
    • 这样写等同于直接定义 const memoizedCallback = () => { doSomething(a, b); }

    • ⚠️ 不建议使用,useCallback 的核心价值就是缓存函数引用

  • 【场景 2】依赖变化时更新函数(常用):

    • 依赖项变化时才创建新函数,否则返回缓存的函数

      // 场景:事件处理 - 仅 a 或 b 变化时才创建新函数
      const memoizedCallback = useCallback(() => {
        doSomething(a, b);
      }, [a, b]); // 依赖 a 和 b
      
    • 只有当 ab 发生变化时,才会创建新的函数

    • 其他状态变化导致组件重新渲染时,函数引用保持不变

  • 【场景 3】仅首次创建函数(常用):

    • 只在首次渲染时创建一次函数,后续永远返回同一个函数引用

      // 场景:事件监听 - 函数引用永不变化
      const handleClick = useCallback(() => {
        console.log("clicked");
      }, []); // 空依赖数组
      
      useEffect(() => {
        window.addEventListener("click", handleClick);
        return () => window.removeEventListener("click", handleClick);
      }, [handleClick]); // handleClick 引用不变,effect 只执行一次
      
    • 适用于不依赖任何外部变量的纯事件处理函数

    • 保证函数引用稳定,避免触发不必要的 effect 重新执行

  • 【场景 4】传递给子组件的回调(常用):

    • 避免因父组件重新渲染而导致子组件不必要的重渲染

      function Parent() {
        const [count, setCount] = useState(0);
        const [other, setOther] = useState(0);
      
        // 只在 count 变化时创建新函数
        const handleClick = useCallback(() => {
          console.log(count);
        }, [count]);
      
        return (
          <div>
            <button onClick={() => setOther(other + 1)}>更新 other</button>
            <Child onClick={handleClick} /> {/* other 变化时,Child 不会重渲染 */}
          </div>
        );
      }
      
      // 需要配合 React.memo 使用
      const Child = React.memo(({ onClick }) => {
        console.log("Child rendered");
        return <button onClick={onClick}>Click me</button>;
      });
      
    • 如果不用 useCallback,每次父组件渲染都会创建新的 handleClick

    • 即使 count 没变,新函数的引用也不同,导致子组件重新渲染

    • 注意: 必须配合 React.memo 才有效果

  • 【场景 5】ref 回调函数测量 DOM(常用):

    • 使用 ref 回调函数来获取和测量 DOM 节点

      function MeasureExample() {
        const [height, setHeight] = useState(0);
      
        // 空依赖:ref 回调函数引用保持不变
        const measuredRef = useCallback((node) => {
          if (node !== null) {
            setHeight(node.getBoundingClientRect().height);
          }
        }, []); // 空数组,函数永不更新
      
        return (
          <>
            <h1 ref={measuredRef}>Hello, world</h1>
            <h2>The above header is {Math.round(height)}px tall</h2>
          </>
        );
      }
      
    • ref 回调在节点挂载时会被调用,传入 DOM 节点

    • 使用 useCallback 确保 ref 函数引用稳定

    • 如果需要响应某些依赖的变化重新测量,可以添加依赖项:

      // 当 fontSize 变化时,重新测量高度
      const measuredRef = useCallback(
        (node) => {
          if (node !== null) {
            setHeight(node.getBoundingClientRect().height);
          }
        },
        [fontSize],
      ); // 依赖 fontSize
      

依赖数组的区别

场景依赖数组函数更新时机使用场景
场景 1每次渲染都创建⚠️ 不建议
场景 2[dep]首次 或 依赖变化常用(事件处理)
场景 3[]仅首次渲染常用(事件监听)
场景 4[dep]首次 或 依赖变化常用(传递给子组件)
场景 5[]仅首次渲染常用(ref 回调测量 DOM)

useCallback vs useMemo

Hook缓存的内容等价写法使用场景
useCallback函数本身useCallback(fn, deps)缓存事件处理器
useMemo函数返回值useMemo(() => fn, deps)缓存计算结果
等价关系-useCallback(fn) === useMemo(() => fn)-

记忆方式:

  • useCallback 缓存函数 → 返回 函数本身
  • useMemo 缓存值 → 返回 函数执行的结果
// 两种写法等价
const fn1 = useCallback(() => a + b, [a, b]);
const fn2 = useMemo(() => () => a + b, [a, b]);

// fn1 === fn2  // true

注意事项

  • useCallback 是性能优化手段,不要过度使用,简单函数直接写就好
  • 必须配合 React.memoshouldComponentUpdate 才能真正避免子组件重渲染
  • 不要为了用而用,先测量性能瓶颈,再优化
  • useCallback 缓存的是函数引用,不是函数的执行结果