🌹useMemouse和Callback钩子详解

42 阅读3分钟

摸鱼仔无事,翻了翻项目代码,发现 useMemo / useCallback 使用相对较少,作为性能优化得关键hooks,不能不深入了解下它们。

今天这篇文章不讲 API 表面,而是:

为什么需要它们?什么时候该用?不用会发生什么?


总览

  • useMemo:缓存「计算结果」
  • useCallback:缓存「函数引用」
  • 它们解决的不是“性能慢”,而是:

无意义的重新计算 / 无意义的重新渲染


1️. 一个真实的问题场景

我们先从一个完全真实、你一定写过的组件开始。

function ExpensiveList({ list }) {

const total = list.reduce((sum, item) => sum + item.price, 0);

console.log("ExpensiveList render");

return

total: {total}
;

}

父组件:

export default function App() {

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

const list = [

{ id: 1, price: 100 },

{ id: 2, price: 200 },

];

return (

<>

<button onClick={() => setCount(c => c + 1)}>+1

</>

);

}

问题来了 ❗

我只点了 count + 1为什么 ExpensiveList 也 render 了?

答案:

因为 父组件重新 render,子组件默认一定 render


2️. useMemo:缓存的是“计算结果”

问题本质

每一次 render:

list.reduce(...)

都会重新执行 ——

即使 list 根本没变


用 useMemo 改造子组件

import { useMemo } from "react";

function ExpensiveList({ list }: { list: { price: number }[] }) {
  // const total = list.reduce((sum, item) => sum + item.price, 0);
  const total = useMemo(() => {
    console.log("代码执行");
    return list.reduce((sum, item) => sum + item.price, 0);
  }, [list]);

  console.log("ExpensiveList render");

  return <div>total: {total}</div>;
}

export default ExpensiveList;


点击父组件button,发生了什么?

  • render 仍然会发生
  • console.log("代码执行")依然执行了,并没有被缓存
  • useMemo缓存失效?

image.png

为什么?

分析原因为每次点击,父组件必然重新render, const list = [ { id: 1, price: 100 }, { id: 2, price: 200 }, ]; 每一次 render,list 都是新引用,那么怎么解决这个问题尼?

改造父组件代码

  const list = useMemo(
    () => [
      { id: 1, price: 100 },
      { id: 2, price: 200 },
    ],
    []
  );

然后我们再点击button

image.png

  • 控制台依然render
  • useMomo对函数计算结果起到了缓存,未打印 console.log("代码执行");

⚠️ useMemo 不阻止 render,只缓存结果


3. useCallback:缓存的是“函数本身”

一个经典翻车现场

function Child({ onClick }) {

console.log("Child render");

return child btn;

}

父组件:

export default function App() {

const [count2, setCount2] = useState(0);

const handleClick = () => {

console.log("click");

};

return (

<>

<button onClick={() => setCount(c => c + 1)}>+1

</>

);

}

现象

+1,Child 每次都 render

image.png

原因

函数也是对象

() => {} !== () => {}

React中,父组件重新执行(render) → JSX 重新创建 → 子组件函数被重新调用(render)


useCallback 登场

const handleClick = useCallback(() => {

console.log("click");

}, []);

结果

  • 函数引用稳定
  • Child 还是会render,因为组件更新,子组件一定render (感觉好绕)

4. useCallback + React.memo 才是完全体


const Child = React.memo(function Child({ handleClick }: { handleClick: () => void }) {

    console.log("Child render");
    
    return <button onClick={    handleClick}>child btn</button>;
    
});
    
export default Child;

image.png

可以看到,当我们父组件state变化得时候,count2变化,父组件render,子组件并没有render

注:

单独 useCallback 没用

useCallback + memo (React.memo) 才能阻止子组件 render


5. 闭包陷阱:useCallback 最容易踩的坑

const handleClick = useCallback(() => {

console.log(count);

}, []);

永远是初始值,其实会发现useEffect,useCallback,useMemo写法使用上还挺像得

为什么?

useCallback 缓存的是:

第一次 render 的函数快照


正确写法 ①:加依赖

const handleClick = useCallback(() => {

console.log(count);

}, [count]);


正确写法 ②:函数式更新(最推荐)

const handleClick = useCallback(() => {

setCount(c => c + 1);

}, []);

函数式更新 = 闭包终结者


6. useMemo vs useCallback 本质对比

hook缓存的是什么返回值
useMemo计算结果任意值
useCallback函数函数本身(配合React.memo使用)

用法总结

useCallback(fn) === useMemo(() => fn) useCallback缓存函数,useMemo缓存计算结果


7. 什么时候不该用?

小组件 没有传给 memo 子组件 计算非常轻

过度 useMemo = 性能负优化


8. 最终总结

  • useMemo 解决的是:重复计算
  • useCallback 解决的是:函数引用不稳定
  • render ≠ 重新计算
  • 闭包问题来自“渲染快照”
  • 函数式更新可以绕开依赖地狱

🔚 面试必杀句

useCallback 是为了解决什么问题?

为了保证函数引用稳定,配合 memo 避免子组件无意义重新渲染,同时要注意闭包和依赖问题。

  • useCallback 几乎总是React.memo 成对出现

  • useMemo 不依赖 React.memo 才能生效,可单独使用,useMemo(()=>{},value),value值不变,就不会重新计算

  • render 优化和计算缓存是两条完全不同的优化路径