useMemo 和 useCallback 有什么区别?使用场景?

一、是什么

useMemouseCallback 是 React 提供的两个性能优化 Hooks,核心目的是在组件重渲染时避免不必要的重复计算和引用变化。

const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
const memoizedFn = useCallback((x) => doSomething(x, id), [id]);

简单来说:

  • useMemo:缓存计算结果(值),只有依赖变化时才重新计算
  • useCallback:缓存函数引用,只有依赖变化时才返回新函数

实际上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps),只不过 useCallback 是针对函数引用这一高频场景的语法糖。

二、useMemo 详解

基本用法

function ProductList({ products, filter }) {
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === filter)
                   .sort((a, b) => a.price - b.price);
  }, [products, filter]);

  return (
    <ul>
      {filteredProducts.map(p => (
        <li key={p.id}>{p.name} - ¥{p.price}</li>
      ))}
    </ul>
  );
}

productsfilter 没有变化时,filteredProducts 会直接复用上次的计算结果,跳过 filter + sort 操作。

缓存对象/数组的引用稳定性

在 React 中,每次渲染都会创建新的对象和数组引用。即使内容相同,{} !== {} 会导致子组件不必要的重渲染:

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

  // 每次 Parent 渲染都创建新的 style 对象
  // 导致 Child 即使用了 React.memo 也会重渲染
  const style = { color: 'red', fontSize: 14 };

  // 用 useMemo 保持引用稳定
  const stableStyle = useMemo(() => ({ color: 'red', fontSize: 14 }), []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <MemoizedChild style={stableStyle} />
    </>
  );
}

const MemoizedChild = React.memo(function Child({ style }) {
  console.log('Child rendered');
  return <div style={style}>内容</div>;
});

作为 Hook 依赖

当计算结果被用作其他 Hook 的依赖时,引用稳定性至关重要:

function ChatRoom({ roomId, serverUrl }) {
  const options = useMemo(
    () => ({ serverUrl, roomId }),
    [serverUrl, roomId]
  );

  useEffect(() => {
    const conn = createConnection(options);
    conn.connect();
    return () => conn.disconnect();
  }, [options]); // options 引用稳定,不会导致不必要的重连
}

三、useCallback 详解

基本用法

function SearchPage() {
  const [query, setQuery] = useState('');

  const handleSearch = useCallback((searchQuery) => {
    fetch(`/api/search?q=${searchQuery}`).then(/* ... */);
  }, []);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <MemoizedSearchResults onSearch={handleSearch} query={query} />
    </>
  );
}

配合 React.memo 使用

useCallback 最常见的用途是与 React.memo 配合,防止子组件因为父组件传递的回调函数引用变化而重渲染:

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const handleAdd = useCallback(() => {
    setTodos(prev => [...prev, { id: Date.now(), text: input, done: false }]);
    setInput('');
  }, [input]);

  const handleToggle = useCallback((id) => {
    setTodos(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button onClick={handleAdd}>添加</button>
      <TodoList
        todos={todos}
        onToggle={handleToggle}
        onDelete={handleDelete}
      />
    </div>
  );
}

const TodoList = React.memo(function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onToggle={onToggle} onDelete={onDelete} />
      ))}
    </ul>
  );
});

作为其他 Hook 的依赖

function useDataFetcher(fetchFn) {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchFn().then(setData);
  }, [fetchFn]); // 如果 fetchFn 不稳定,每次渲染都会重新请求

  return data;
}

function UserPage({ userId }) {
  const fetchUser = useCallback(
    () => fetch(`/api/users/${userId}`).then(r => r.json()),
    [userId]
  );
  const user = useDataFetcher(fetchUser);
  return <div>{user?.name}</div>;
}

四、依赖数组的工作机制

React 使用 Object.is 进行浅比较来判断依赖是否变化:

// 基本类型:值比较
Object.is(1, 1)           // true
Object.is('a', 'a')       // true

// 引用类型:引用比较
Object.is({}, {})         // false
Object.is([1,2], [1,2])   // false

const obj = {};
Object.is(obj, obj)       // true

常见陷阱——在渲染阶段创建的对象每次都是新引用:

function App({ items }) {
  // 错误:每次渲染都是新数组,useMemo 的依赖永远在"变化"
  const sorted = useMemo(
    () => items.slice().sort(),
    [items.filter(i => i.active)] // 每次创建新数组
  );

  // 正确:依赖应该是稳定的引用或基本类型
  const activeCount = items.filter(i => i.active).length;
  const sorted2 = useMemo(
    () => items.filter(i => i.active).sort(),
    [items, activeCount]
  );
}

五、什么时候该用 / 不该用

适合使用的场景

  1. 计算量大的派生数据:对大数组进行 filter/sort/reduce
  2. 引用稳定性保证:传给 memo 化子组件的 props
  3. 作为 Hook 依赖:避免 useEffect 等无限循环
  4. Context value 缓存:防止所有消费者不必要地重渲染
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  // 缓存 value 避免每次渲染创建新对象
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}

不该使用的场景(过早优化)

function SimpleComponent({ name }) {
  // 不需要:字符串拼接代价极低
  const greeting = useMemo(() => `Hello, ${name}!`, [name]);

  // 不需要:没有配合 React.memo 的子组件
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  // 直接写就好
  return <div onClick={() => console.log('clicked')}>Hello, {name}!</div>;
}

useMemo/useCallback 本身有成本:额外的函数调用、依赖数组的比较、内存占用。如果缓存的计算量小于这些开销,反而会降低性能。

六、React Compiler 与自动 Memoization

React 19 引入了 React Compiler(原名 React Forget),目标是在编译阶段自动插入 memoization,让开发者不再需要手动写 useMemo 和 useCallback:

// 开发者只需写朴素代码
function ProductList({ products, filter }) {
  const filtered = products.filter(p => p.category === filter);
  return filtered.map(p => <ProductCard key={p.id} product={p} />);
}

// React Compiler 编译后自动插入缓存逻辑(伪代码)
function ProductList({ products, filter }) {
  const $ = _c(2);
  let filtered;
  if ($[0] !== products || $[1] !== filter) {
    filtered = products.filter(p => p.category === filter);
    $[0] = products;
    $[1] = filter;
    $[2] = filtered;
  } else {
    filtered = $[2];
  }
  // ...
}

React Compiler 的前提是代码遵循 React 规则(纯渲染、不修改 props/state),它通过静态分析自动推断哪些值需要缓存。

在 Compiler 全面落地前,手动使用 useMemo/useCallback 仍然是必要的优化手段。

七、总结

对比维度useMemouseCallback
缓存对象计算结果(任意值)函数引用
返回值缓存的值缓存的函数
等价关系useMemo(() => fn, deps)
典型场景昂贵计算、稳定对象引用memo 化子组件回调、Hook 依赖
注意事项不要缓存廉价计算不配合 memo 则意义不大

核心原则:先写正确的代码,再用 Profiler 定位性能瓶颈,最后有针对性地加入 memoization。不要为了"可能的性能问题"把所有值和函数都包一层 useMemo/useCallback,这是典型的过早优化。