useCallback 是 React 的一个 Hook,它用于 缓存函数,以便在组件的重新渲染中 保持函数引用不变,避免不必要的重新渲染或副作用执行。

为什么使用 useCallback

React 在组件渲染时,会重新创建每个在组件中定义的函数。如果这个函数作为 useEffectuseMemo 的依赖,或者传递给子组件,它的每次重新定义都会导致依赖的副作用重新执行或子组件重新渲染,这可能会导致性能问题,尤其是在子组件依赖这些函数时。

useCallback 可以帮助我们在依赖项没有变化的情况下 避免函数重新创建

useCallback 的语法

const memoizedCallback = useCallback(
  () => {
    // Your callback function logic here
  },
  [dependencies] // 依赖项数组,当依赖项变化时,才会重新创建函数
);
  • 第一个参数:你希望缓存的函数。
  • 第二个参数:依赖项数组,当数组中的任何依赖项发生变化时,useCallback 会返回一个新的函数,否则它会返回缓存的函数。

使用 useCallback 的常见场景

1. 优化传递给子组件的回调函数

如果一个函数被传递给子组件,并且这个子组件通过 React.memoPureComponent 来优化性能(避免不必要的重新渲染),我们通常需要确保传递给子组件的函数在父组件渲染时保持引用不变。否则,子组件会因为函数引用变化而重新渲染。

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

const ChildComponent = React.memo(({ handleClick }) => {
  console.log('ChildComponent rendered');
  return <button onClick={handleClick}>Click me</button>;
});

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

  // 使用 useCallback 缓存 handleClick 函数
  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 依赖 count,只有 count 变化时,handleClick 才会更新

  return (
    <div>
      <ChildComponent handleClick={handleClick} />
      <p>Count: {count}</p>
    </div>
  );
};

export default ParentComponent;
  • useCallback 确保 handleClick 函数在 count 不变的情况下不会重新创建,从而避免了 ChildComponent 的不必要重新渲染。

    2. 防止不必要的副作用执行

如果某个函数在 useEffectuseMemo 中被使用,并且这个函数被当作依赖项传递给 useEffect,则每次函数重新创建都会导致 useEffect 被重新执行。如果你希望 useEffect 只在依赖项变化时执行,使用 useCallback 缓存函数可以防止这种情况。

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

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

  const handleInit = useCallback(() => {
    console.log('Initializing...');
  }, []); // handleInit 函数不会改变

  useEffect(() => {
    handleInit();
  }, [handleInit]); // 只有 handleInit 改变时才会执行

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <p>Count: {count}</p>
    </div>
  );
};
  • 在这个例子中,handleInit 只有在组件挂载时执行一次,因为它被 useCallback 缓存,且没有依赖项。

使用 useCallback 的注意事项

  1. 避免滥用

    • useCallback 只是为了性能优化。不要为了避免函数重新创建而滥用它,只有在函数作为 useEffect 或子组件的依赖时才需要使用。
    • 如果一个函数在组件内频繁变化,并且它不传递给子组件或不用于副作用中,使用 useCallback 可能没有实际效果,反而会增加代码复杂度。
  2. useMemo 的区别

    • useMemo 用于缓存,而 useCallback 用于缓存函数。这两个 Hook 的本质相同,都是通过依赖项控制缓存。
    const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b]);
    const memoizedCallback = useCallback(() => expensiveComputation(a, b), [a, b]);
    

总结

useCallback 是 React 中用于优化性能的工具,特别是在函数作为子组件 props 或 useEffect 的依赖时,可以避免不必要的重新渲染或副作用执行。需要谨慎使用,只在必要时才用,避免过度优化导致代码复杂性增加。

代码 1:使用 useCallback 包裹 getPersonList 函数

const getPersonList = useCallback(async () => {
  try {
    const res = await apiUtil.getPersonList({
      userId: userInfoStore.userInfo?.id,
      pageSize,
      pageNum,
    });
    console.log('res', res);
    setPersonList(res.data);
  } catch (e) {
    console.log(e);
  }
}, [pageSize, pageNum, userInfoStore.userInfo?.id]);

useEffect(() => {
  getPersonList();
}, [getPersonList]);

代码 2:直接在 useEffect 中调用 getPersonList

const getPersonList = async () => {
  try {
    const res = await apiUtil.getPersonList({
      userId: userInfoStore.userInfo?.id,
      pageSize,
      pageNum,
    });
    console.log('res', res);
    setPersonList(res.data);
  } catch (e) {
    console.log(e);
  }
};

useEffect(() => {
  getPersonList();
}, [pageSize, pageNum, userInfoStore.userInfo?.id]);

区别

1. useCallbackuseEffect 的依赖管理

  • 代码 1(使用 useCallback

    • getPersonList 是通过 useCallback 包裹的,这意味着 getPersonList 函数本身只会在其依赖项发生变化时(即 pageSize, pageNum, 和 userInfoStore.userInfo?.id)重新创建。如果这些依赖项没有发生变化,getPersonList 会保持引用不变。
    • useEffect 的依赖项是 getPersonList,这意味着 useEffect 只有在 getPersonList 的引用发生变化时才会重新执行。因为 getPersonList 是通过 useCallback 包裹的,所以只有在其依赖项变化时(如 pageSize, pageNum, userInfoStore.userInfo?.id)才会触发 useEffect
    • 优势:通过 useCallback,你确保了 getPersonList 的函数在依赖项不变时不会被重新创建,从而避免了不必要的重新渲染和 useEffect 执行。
  • 代码 2(直接在 useEffect 中调用)

    • getPersonList 函数没有被包裹在 useCallback 中,每次 useEffect 执行时,都会重新创建一个新的 getPersonList 函数实例。
    • useEffect 会依赖 pageSize, pageNum, 和 userInfoStore.userInfo?.id,并且每次这些值发生变化时,都会重新执行 getPersonList 函数,即使它是直接定义的。
    • 问题:每次组件渲染或依赖项变化时,getPersonList 都会被重新创建,虽然它本身不会直接影响 useEffect 执行,但可能会导致不必要的重新渲染或性能问题,特别是如果 getPersonList 很复杂或涉及一些其他的计算。

2. 性能优化

  • 代码 1 使用 useCallback 可以防止在每次渲染时都重新创建 getPersonList 函数。useCallback 会在依赖项变化时返回同一个函数实例,从而避免了不必要的函数重建。

    • 优化场景:如果 getPersonList 被传递给子组件或用作事件处理程序,使用 useCallback 可以避免子组件的重新渲染或事件处理程序的重新绑定。
  • 代码 2 没有使用 useCallback,每次组件渲染时都会重新定义一个新的 getPersonList 函数,这在某些场景下可能导致不必要的性能开销,尤其是当组件较为复杂或频繁更新时。

3. 语义上的差异

  • 代码 1 更加明确地表明 getPersonList 是一个“稳定的”函数,只有在其依赖项发生变化时才会重新创建,并且通过 useEffect 监听其变化。这种方式更适合处理复杂的异步函数,或者当 getPersonList 被传递给其他组件时(例如作为 props)。

  • 代码 2 更加简单和直观,因为它直接在 useEffect 中定义和调用 getPersonList,没有额外的包裹,适合逻辑比较简单且不需要优化的场景。

总结

  • 代码 1 是更为“优化”的做法,通过 useCallback 使 getPersonList 函数稳定,并且避免不必要的函数重建和 useEffect 执行。
  • 代码 2 更简单,但可能会在某些情况下导致性能问题,尤其是当 getPersonList 变得复杂或与其他优化需求相关时。

如果没有传递 getPersonList 到子组件或者没有复杂的依赖关系,代码 2 也可以正常工作。如果有性能需求或复杂的函数逻辑,代码 1 是更推荐的做法。