跳至正文
来两杯美式
返回

React 核心(8):useCallback 与 useMemo

By 来两杯美式
发布于更新于

useCallback 与 useMemo

一句话定义:useCallback 缓存函数引用,useMemo 缓存计算结果——它们解决的是同一个问题:引用稳定性


一、为什么需要这两个 Hook?

React 组件每次重渲染,函数体都会从头到尾重新执行一遍。这意味着:

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // 每次渲染都重新创建!
  const handleClick = () => {
    console.log("clicked");
  };
  const config = { theme: "dark", lang: "zh" };

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <Child onClick={handleClick} config={config} />
    </div>
  );
}

text 变了 → Parent 重渲染 → handleClickconfig 重新创建 → 传给 Child 的 props 引用变了 → Child 也跟着重渲染。

handleClickconfig 的内容明明没变!子组件完全不需要重新渲染。

这就是问题所在:JavaScript 的引用相等性

1.1 核心问题:引用相等性

{} === {}           // false —— 两个空对象,但不是同一个引用
() => {} === () => {} // false —— 两个空函数,但不是同一个引用

const obj = {}
const fn = () => {}
obj === obj  // true —— 同一个引用
fn === fn    // true —— 同一个引用

每次渲染创建的新函数/新对象,和上次创建的内容一样,但引用不同。React 的浅比较(Object.is)只看引用,不看内容——引用变了,就认为 props 变了。

类比:你每天穿同款衣服上班,同事还是认得出你。但 JavaScript 不认”同款”,只认”是不是同一件”——换了一件新的,即使一模一样,它也认为你变了。

1.2 什么时候引用不稳定会出问题?

不是所有引用不稳定都值得修复。只有当引用变了导致不必要的重渲染时,才是问题:

// 场景 1:传给 memo 子组件 → 引用变了 → memo 失效 → 子组件白渲染
<MemoChild onClick={handleClick} />;

// 场景 2:引用作为 useEffect 依赖 → 引用变了 → effect 白执行
useEffect(() => {
  fetchData(filter);
}, [filter]);

如果子组件没用 React.memo,或者引用不是 effect 依赖,那引用变不变都无所谓——反正子组件本来就会跟着父组件渲染。


二、useCallback:缓存函数引用

2.1 语法

const cachedFn = useCallback(fn, deps);
//                    ↑ 要缓存的函数  ↑ 依赖数组

2.2 做了什么

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // text 变了 → Parent 重渲染 → 但 handleClick 引用不变!
  const handleClick = useCallback(() => {
    console.log("clicked");
  }, []); // 空依赖 → 永远返回同一个函数

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <MemoChild onClick={handleClick} />
    </div>
  );
}

类比:useCallback 像名片——给别人的永远是同一张名片,而不是每次见面重新印一张。别人(子组件)一看,名片没换,就知道你还是你。

2.3 依赖数组怎么写

依赖数组里的值变了,函数才会重新创建:

function SearchBar({ projectId }) {
  const [query, setQuery] = useState("");

  // projectId 或 query 变了 → 重新创建函数(闭包里要用新值)
  const handleSearch = useCallback(() => {
    fetchResults(projectId, query);
  }, [projectId, query]); // 依赖了什么就写什么

  return <MemoResults onSearch={handleSearch} />;
}

规则和 useEffect 的依赖数组完全一样:函数里用到了哪些外部变量,就写进 deps。漏写了 → 闭包里读到旧值(闭包陷阱,见 useState完全指南 §5.1)。

2.4 什么时候用 / 不用 useCallback

场景用不用原因
回调传给 React.memo 子组件✅ 用引用稳定 → memo 才能生效
回调作为 useEffect / useMemo 依赖✅ 用引用稳定 → 避免反复执行
内联事件处理器传给原生 DOM❌ 不用原生标签没有 memo,引用变不变都一样
只在组件内自用的函数❌ 不用没人关心它的引用
子组件没用 React.memo❌ 不用子组件本来就会跟着父组件渲染
// ✅ 传给 memo 子组件
const MemoList = React.memo(function List({ onSelect }) {
  return items.map(item => (
    <li key={item.id} onClick={() => onSelect(item.id)} />
  ));
});

function Parent() {
  const handleSelect = useCallback(id => {
    setSelected(id);
  }, []); // 依赖为空 → 引用永远不变 → MemoList 不重渲染

  return <MemoList onSelect={handleSelect} />;
}
// ✅ 作为 useEffect 依赖
function DataFetcher({ userId }) {
  const [data, setData] = useState(null);

  const fetchUser = useCallback(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setData);
  }, [userId]); // userId 变了才重新创建

  useEffect(() => {
    fetchUser();
  }, [fetchUser]); // fetchUser 引用稳定 → effect 不会反复触发

  return <div>{data?.name}</div>;
}
// ❌ 没必要——原生 <input> 不做 props 浅比较
function Form() {
  const [value, setValue] = useState("");
  // ✅ 直接内联就行
  return <input value={value} onChange={e => setValue(e.target.value)} />;
}

判断标准:这个函数的引用会不会被别人”观察”?如果会(传给 memo 子组件、作为 effect 依赖),就用;如果不会,就不用。


三、useMemo:缓存计算结果

3.1 语法

const cachedValue = useMemo(() => computeValue, deps);
//                        ↑ 计算函数(返回要缓存的值)  ↑ 依赖数组

注意:useMemo 的第一个参数是工厂函数() => 值),不是值本身。

3.2 做了什么

function ProductList({ products, filterText }) {
  // filterText 变了才重新过滤
  const filteredProducts = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [products, filterText]);

  return <MemoList items={filteredProducts} />;
}

类比:useMemo 像备忘录——算过的结果记下来,下次同样的输入直接用,不用重新算。输入变了才重新算一遍。

3.3 什么时候用 / 不用 useMemo

场景用不用原因
计算结果作为子组件 props✅ 用引用稳定 → memo 才能生效
计算结果作为 useEffect 依赖✅ 用引用稳定 → effect 不会反复执行
昂贵计算✅ 用避免每次渲染重复计算
简单计算(加法、字符串拼接)❌ 不用计算成本 < 缓存成本
每次渲染都需要新引用❌ 不用缓存了反而坏事
// ✅ 计算结果传给 memo 子组件
function Dashboard({ data, selectedId }) {
  const selectedItems = useMemo(() => {
    return data.filter(item => item.categoryId === selectedId);
  }, [data, selectedId]); // 不用 useMemo → 每次渲染创建新数组 → memo 失效

  return <MemoChart items={selectedItems} />;
}
// ✅ 昂贵计算
function DataViz({ rawPoints }) {
  // 10000 个点的聚类计算,每次渲染都跑一遍太浪费
  const clusters = useMemo(
    () => expensiveClusterAlgorithm(rawPoints),
    [rawPoints]
  );
  return (
    <svg>
      {clusters.map(c => (
        <Circle key={c.id} {...c} />
      ))}
    </svg>
  );
}
// ❌ 简单计算不需要缓存
function Greeting({ firstName, lastName }) {
  // ❌ 字符串拼接不费性能,缓存反而多了 deps 比较的开销
  const fullName = useMemo(
    () => `${firstName} ${lastName}`,
    [firstName, lastName]
  );

  // ✅ 直接算——字符串是原始类型,每次结果一样,不会导致 memo 失效
  const fullName = `${firstName} ${lastName}`;
}

判断标准:这个值的引用会不会被别人”观察”?计算是否昂贵?两个条件满足任一就用,都不满足就不用。


四、React.memo 配合:三者联合作战

4.1 React.memo 做了什么

React.memo 是高阶组件,对组件的 props 做浅比较——如果每个 prop 的引用都没变,就跳过重渲染:

const MemoChild = React.memo(function Child({ onClick, config }) {
  // 只有 onClick 或 config 的引用变了,才会重新渲染
  return <div onClick={onClick}>{config.label}</div>;
});

4.2 不用 useCallback/useMemo → memo 失效

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

  // 每次渲染都创建新函数和新对象
  const handleClick = () => {
    /* ... */
  };
  const config = { label: "Click me" };

  // count 变了 → Parent 重渲染 → handleClick 和 config 引用变了
  // → MemoChild 的 props 引用变了 → 浅比较失败 → memo 失效 → 白渲染
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <MemoChild onClick={handleClick} config={config} />
    </div>
  );
}

4.3 三者配合的完整模式

const MemoChild = React.memo(function Child({ onClick, items }) {
  console.log("Child rendered"); // 只在 props 真正变化时打印
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function Parent({ projectId }) {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // ✅ useCallback 稳定函数引用
  const handleSelect = useCallback(id => {
    console.log("Selected:", id);
  }, []); // 无外部依赖 → 引用永远不变

  // ✅ useMemo 稳定计算结果引用
  const visibleItems = useMemo(() => {
    return items.filter(item => item.projectId === projectId);
  }, [items, projectId]);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* count 变了 → Parent 重渲染 → 但 handleSelect 和 visibleItems 引用不变 */}
      {/* → MemoChild 的 props 没变 → memo 生效 → Child 不重渲染 */}
      <MemoChild onClick={handleSelect} items={visibleItems} />
    </div>
  );
}

完整模式React.memo 守门(浅比较 props),useCallback 稳定函数引用,useMemo 稳定对象/数组引用。三者缺一——memo 就可能失效。


五、常见过度优化陷阱

5.1 过度使用 useMemo 缓存简单值

function UserCard({ name, age }) {
  // ❌ 加法、字符串拼接——计算成本几乎为零,缓存反而多了 deps 比较的开销
  const birthYear = useMemo(() => 2026 - age, [age]);
  const displayName = useMemo(() => name.toUpperCase(), [name]);

  // ✅ 直接算,简单明了
  const birthYear = 2026 - age;
  const displayName = name.toUpperCase();
}

原则:计算成本 < 缓存成本(deps 比较 + 闭包维护)时,不要用 useMemo

5.2 到处用 useCallback

function Page() {
  const [text, setText] = useState("");

  // ❌ 只传给原生 <button>,没人 memo 它
  const handleClick = useCallback(() => {
    setText("");
  }, []);
  // ❌ 只在组件内自用
  const formatText = useCallback(t => t.trim().toLowerCase(), []);

  // ✅ 直接写,代码更简洁
  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setText("")}>Clear</button>
      <p>{text.trim().toLowerCase()}</p>
    </div>
  );
}

5.3 正确的心智模型

先写正确的代码,遇到性能问题再优化,用 React DevTools Profiler 量化。

  1. 先写对:不用任何缓存 Hook,让代码简洁可读
  2. 遇到问题:用 React DevTools Profiler 找到重渲染瓶颈
  3. 针对性优化:只在 Profiler 证明有问题的地方加 useCallback/useMemo/React.memo
// 第一步:先写对,不管性能
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    /* ... */
  };
  return <Child onClick={handleClick} />;
}

// 第二步:Profiler 发现 Child 重渲染太频繁
// 第三步:针对性加缓存
const MemoChild = React.memo(Child);
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = useCallback(() => {
    /* ... */
  }, []);
  return <MemoChild onClick={handleClick} />;
}

过早优化是万恶之源。React 的重渲染通常很快——大部分时候你不需要这些 Hook。先用 Profiler 证明有问题,再动手。


六、依赖数组与 useState setter 的特殊规则

6.1 useState setter 引用天然稳定

useState 返回的 setter 函数,引用在组件整个生命周期内不变

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

  // setCount 的引用永远不变——React 保证这一点
  useEffect(() => {
    const timer = setInterval(() => setCount(c => c + 1), 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖没问题,setCount 引用稳定
}
// ❌ 多此一举——setCount 本身就是稳定的
const stableSetCount = useCallback(setCount, [])

// ✅ 直接用
<Child onChange={setCount} />

6.2 为什么 setter 是稳定的?

setter 不依赖任何外部变量——它内部用函数形式 prev => newValue 拿最新值,不需要闭包捕获。React 在内部实现时就保证了同一个 setter 引用贯穿组件生命周期。

记住useState 的 setter、useRef.currentdispatch(来自 useReducer)——这些引用天然稳定,不需要 useCallback 包裹。


七、useCallback 和 useMemo 的关系

7.1 本质上是同一个东西

useCallbackuseMemo 的语法糖:

// 这两行等价
useCallback(fn, deps);
useMemo(() => fn, deps);

useMemo 缓存的是工厂函数的返回值。当工厂函数 () => fn 返回一个函数时,缓存的就是这个函数——和 useCallback 一模一样。

7.2 什么时候用哪个

需求用哪个原因
缓存函数useCallback语义更清晰,少写一层箭头函数
缓存对象/数组/计算结果useMemouseCallback 做不到
// 缓存函数 → 用 useCallback(更简洁)
const handleClick = useCallback(() => setSelected(id), [id]);

// 等价的 useMemo 写法(多一层箭头函数,没必要)
const handleClick = useMemo(() => () => setSelected(id), [id]);

// 缓存对象 → 只能用 useMemo
const config = useMemo(
  () => ({
    method: "GET",
    headers: { auth: token },
  }),
  [token]
);

简单记忆:缓存函数用 useCallback,缓存其他一切用 useMemo


八、一张图总结

                    引用不稳定的问题

              ┌──────────┼──────────┐
              ▼          ▼          ▼
         函数引用变   对象/数组引用变  昂贵计算重复
              │          │          │
              ▼          ▼          ▼
         useCallback   useMemo    useMemo
              │          │          │
              └──────────┼──────────┘

                   引用稳定了

              ┌──────────┼──────────┐
              ▼          ▼          ▼
         memo 生效   effect 不反复   计算不浪费

使用决策流程

  1. 这个函数/值的引用会被别人”观察”吗?(传给 memo 子组件、作为 effect 依赖)
    • 不会 → 不用缓存
    • 会 → 继续
  2. 它的引用天然稳定吗?(useState setter、useRef、dispatch)
    • 是 → 不用缓存
    • 不是 → 用 useCallback(函数)或 useMemo(其他)

九、一句话

useCallbackuseMemo 解决引用稳定性问题——引用变了,React.memo 失效、useEffect 反复执行。但大部分时候你不需要它们:先写对,用 Profiler 证明有问题,再针对性优化。useState 的 setter 天然稳定,不用包。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
React
第 8 / 17 篇
查看系列全部文章
  1. 01.React 核心(1):React 组件与函数的本质区别
  2. 02.React 核心(2):声明式与 React 的实现机制
  3. 03.React 核心(3):Props 完全指南
  4. 04.React 核心(4):useState 完全指南
  5. 05.React 核心(5):useEffect 完全指南
  6. 06.React 核心(6):useRef 完全指南
  7. 07.React 核心(7):Context API 与状态共享
  8. 08.React 核心(8):useCallback 与 useMemo
  9. 09.React 核心(9):Hooks 规则与闭包陷阱
  10. 10.React 核心(10):自定义 Hook 设计
  11. 11.React 核心(11):Key 与列表渲染
  12. 12.React 核心(12):表单与受控/非受控组件
  13. 13.React 核心(13):React 渲染机制深入——Fiber 与调度
  14. 14.React 核心(14):React 18 并发特性
  15. 15.React 核心(15):Suspense 机制详解
  16. 16.React 核心(16):TanStack Query 入门指南
  17. 17.React 核心(17):TanStack Query 缓存机制详解

上一篇
React 核心(9):Hooks 规则与闭包陷阱
下一篇
React 核心(7):Context API 与状态共享