useCallback useMemo memo 三个区别和作用

useCallback 用于缓存函数,避免在每次渲染时创建新的函数实例

scss 复制代码
const [count, setCount] = useState(0);
const [name, setName] = useState('');

// ❌ 每次渲染都会创建新函数
const handleClick = () => {
  console.log('Button clicked');
};

// ✅ 使用 useCallback 缓存函数
const handleClickMemo = useCallback(() => {
  console.log('Button clicked');
}, []); // 依赖数组为空,函数永远不会重新创建

// ✅ 依赖变化时重新创建函数
const handleNameChange = useCallback((newName: string) => {
  setName(newName);
}, [setName]); // 依赖 setName

useMemo 用于缓存计算结果,避免在每次渲染时重复执行昂贵的计算。

注意

应该只用于"纯函数"计算,即给定相同输入总是返回相同输出,且没有副作用。 因为useMemo中的内容会执行不止一次

不能用于打印日志、网络请求 、开启定时器、给外部数据赋值等等其他任何非纯计算的操作。

ini 复制代码
const [count, setCount] = useState(0);
  const [items, setItems] = useState<number[]>([]);

  // ❌ 每次渲染都执行昂贵计算
  const expensiveValue = items.reduce((sum, item) => {
    // 模拟昂贵的计算
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += item * i;
    }
    return sum + result;
  }, 0);

  // ✅ 使用 useMemo 缓存计算结果
  const expensiveValueMemo = useMemo(() => {
    return items.reduce((sum, item) => {
      // 模拟昂贵的计算
      let result = 0;
      for (let i = 0; i < 1000000; i++) {
        result += item * i;
      }
      return sum + result;
    }, 0);
  }, [items]); // 只有 items 变化时才重新计算

  // ✅ 缓存过滤后的列表
  const filteredItems = useMemo(() => {
    return items.filter(item => item > 5);
  }, [items]);

  // ✅ 缓存复杂对象
  const complexObject = useMemo(() => ({
    data: items,
    count: count,
    timestamp: Date.now(),
    processed: items.map(item => item * 2)
  }), [items, count]);

子组件使用 React.memo 优化

javascript 复制代码
const ChildComponent = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

useCallback 的主要目的是优化性能,但只有在特定条件下才能真正起到优化作用。

注意事项

1. 不要过度使用
2. 依赖数组要完整
3. 避免在 useMemo 中执行副作用

详细解释

1. 没有 React.memo 的情况

javascript 复制代码
typescript
// 子组件没有使用 React.memo
const ChildComponent = ({ onClick }) => {
  console.log('ChildComponent rendered'); // 每次都会打印
  return <button onClick={onClick}>Click me</button>;
};

const Parent = () => {
  const [count, setCount] = useState(0);
  
  // ❌ 没有意义的 useCallback
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);
  
  // ✅ 其实和这样写效果一样
  const handleClick2 = () => {
    console.log('Clicked');
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* 每次 Parent 重新渲染时,ChildComponent 都会重新渲染 */}
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

问题 :即使使用了 useCallback,子组件仍然会在每次父组件重新渲染时重新渲染,因为:

  • 父组件重新渲染时创建新的 JSX
  • <ChildComponent onClick={handleClick} /> 被重新创建
  • React 认为这是一个新的元素,会重新渲染子组件

2. 使用 React.memo 的情况

javascript 复制代码
typescript
// 子组件使用 React.memo
const MemoizedChild = React.memo(({ onClick }) => {
  console.log('MemoizedChild rendered'); // 只有在 props 真正变化时才打印
  return <button onClick={onClick}>Click me</button>;
});

const Parent = () => {
  const [count, setCount] = useState(0);
  
  // ✅ 有意义的 useCallback
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* 只有当 handleClick 真正变化时,MemoizedChild 才会重新渲染 */}
      <MemoizedChild onClick={handleClick} />
    </div>
  );
};

效果

  • 点击 "Increment" 按钮时,count 变化,Parent 重新渲染
  • 但由于 useCallback 的依赖数组为空,handleClick 引用保持不变
  • MemoizedChild 通过 React.memo 检测到 props 没有变化,跳过重新渲染

3. 对比示例

javascript 复制代码
typescript
// 演示区别
const Parent = () => {
  const [count, setCount] = useState(0);
  
  // 没有 useCallback
  const handleClickWithoutCallback = () => {
    console.log('Clicked');
  };
  
  // 有 useCallback
  const handleClickWithCallback = useCallback(() => {
    console.log('Clicked');
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      
      {/* 每次 Parent 重新渲染时,这个子组件都会重新渲染 */}
      <RegularChild onClick={handleClickWithoutCallback} />
      
      {/* 只有当 handleClickWithCallback 引用变化时才重新渲染 */}
      <MemoizedChild onClick={handleClickWithCallback} />
    </div>
  );
};

什么是副作用(Side Effect)?

副作用是指函数除了返回值之外,还做了其他影响程序状态的事情。常见的副作用包括:

1. 控制台日志

ini 复制代码
typescript
const result = useMemo(() => {
  console.log('This is a side effect'); // ❌ 副作用
  return data.filter(item => item.active);
}, [data]);

2. 修改外部变量

ini 复制代码
typescript
let externalVar = 0;

const result = useMemo(() => {
  externalVar++; // ❌ 副作用:修改了外部状态
  return data.map(item => item.value);
}, [data]);

3. 网络请求

scss 复制代码
typescript
const result = useMemo(() => {
  fetch('/api/data'); // ❌ 副作用:网络请求
  return processData(data);
}, [data]);

4. DOM 操作

ini 复制代码
typescript
const result = useMemo(() => {
  document.title = 'New Title'; // ❌ 副作用:修改 DOM
  return formattedData;
}, [data]);

5. 定时器

javascript 复制代码
typescript
const result = useMemo(() => {
  setTimeout(() => { /* ... */ }, 1000); // ❌ 副作用:设置定时器
  return calculatedValue;
}, [deps]);

为什么 useMemo 中不应该有副作用?

1. 不可预测的执行时机

javascript 复制代码
typescript
const Component = () => {
  const [count, setCount] = useState(0);
  
  // ❌ 问题:这个 console.log 的执行时机不可预测
  const result = useMemo(() => {
    console.log('Memo executed'); // 可能执行 0 次、1 次或多次
    return count * 2;
  }, [count]);
  
  return <div>{result}</div>;
};

2. React 的优化机制

scss 复制代码
typescript
// React 可能会跳过 useMemo 的执行
const expensiveValue = useMemo(() => {
  console.log('This might not run'); // ❌ 不可靠
  return heavyCalculation();
}, [deps]);

正确的做法

使用 useEffect 处理副作用:

scss 复制代码
typescript
const Component = () => {
  const [data, setData] = useState([]);
  const [count, setCount] = useState(0);
  
  // ✅ 纯净的计算
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      processed: item.value * 2
    }));
  }, [data]);
  
  // ✅ 副作用放在 useEffect 中
  useEffect(() => {
    console.log('Data changed:', processedData.length);
    // 其他副作用操作
  }, [processedData]); // 依赖处理后的数据
  
  // ✅ 或者依赖原始数据
  useEffect(() => {
    console.log('Count changed to:', count);
  }, [count]);
  
  return <div>{/* 渲染内容 */}</div>;
};

实际例子:

javascript 复制代码
typescript
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  
  // ✅ 纯净的数据处理
  const userDisplayInfo = useMemo(() => {
    if (!user) return null;
    return {
      displayName: `${user.firstName} ${user.lastName}`,
      avatar: user.profileImage || '/default-avatar.png',
      memberSince: new Date(user.createdAt).getFullYear()
    };
  }, [user]);
  
  // ✅ 副作用放在 useEffect 中
  useEffect(() => {
    console.log('User display info updated:', userDisplayInfo);
    // 可以在这里做其他副作用操作,比如发送分析事件
  }, [userDisplayInfo]);
  
  // ✅ 数据获取也应该是副作用
  useEffect(() => {
    const fetchUser = async () => {
      const userData = await fetchUserById(userId);
      setUser(userData);
    };
    fetchUser();
  }, [userId]);
  
  return (
    <div>
      {userDisplayInfo && (
        <div>
          <img src={userDisplayInfo.avatar} alt="Avatar" />
          <h2>{userDisplayInfo.displayName}</h2>
          <p>Member since {userDisplayInfo.memberSince}</p>
        </div>
      )}
    </div>
  );
};

性能成本的解释

useCallback 本身也有成本:

javascript 复制代码
typescript
// useCallback 内部需要:
// 1. 检查依赖数组是否变化
// 2. 决定是否返回缓存的函数
// 3. 如果依赖变化,创建新函数

// 对于简单函数,这个成本可能比重新创建函数还高
const simpleFunction = useCallback(() => {
  return a + b; // 简单计算
}, [a, b]); // 可能得不偿失

最佳实践

ini 复制代码
typescript
// ✅ 什么时候使用 useCallback:
1. 子组件使用了 React.memo
2. 函数作为 props 传递给子组件
3. 函数创建成本较高(复杂逻辑)

// ❌ 什么时候不需要 useCallback:
1. 子组件没有使用 React.memo
2. 函数很简单,重新创建成本很低
3. 函数不作为 props 传递

// ✅ 实际例子:
const ExpensiveComponent = () => {
  const [items, setItems] = useState([]);
  
  // 有价值:复杂的数据处理函数传递给 memoized 子组件
  const processItem = useCallback((item) => {
    // 复杂的处理逻辑
    return heavyProcessing(item);
  }, []);
  
  return (
    <MemoizedItemList 
      items={items} 
      onProcess={processItem} // 传递给 memoized 组件
    />
  );
};

总结

  • useCallback 只有配合 React.memo 才能发挥真正的性能优化作用,否则就是过早优化,反而可能增加不必要的复杂性。
  • useMemo:应该只用于"纯函数"计算,即给定相同输入总是返回相同输出,且没有副作用
  • useEffect:专门用于处理副作用
相关推荐
非ban必选3 小时前
netty-scoket.io路径配置
java·服务器·前端
じòぴé南冸じょうげん3 小时前
小程序的project.private.config.json是无依赖文件,那可以删除吗?
前端·小程序·json
会豪3 小时前
Electron主进程渲染进程如何优雅的进行通信
前端
jianghaha20113 小时前
前端 Word 模板参入特定数据 并且下载
前端·word
跟橙姐学代码3 小时前
轻松搞定 Python 模块与包导入:新手也能秒懂的入门指南
前端·python·ipython
aiwery3 小时前
大模型场景下的推送技术选型:轮询 vs WebSocket vs SSE
前端·agent
会豪3 小时前
前端插件-不固定高度的DIV如何增加transition
前端
却尘3 小时前
Server Actions 深度剖析(2):缓存管理与重新验证,如何用一行代码干掉整个客户端状态层
前端·客户端·next.js
小菜全3 小时前
Vue 3 + TypeScript 事件触发与数据绑定方法
前端·javascript·vue.js