useCallback

缓存函数引用的利器 - 优化子组件渲染性能

核心概述

在 React 组件中,每次重新渲染都会导致组件内的所有代码重新执行, 包括事件处理函数的重新创建。在 JavaScript 中,函数是对象,每次创建都会得到新的引用。 当这些函数作为 props 传递给经过 React.memo 优化的子组件时, 新的函数引用会导致子组件即使 props 值没变也会重新渲染,破坏性能优化。

useCallback 通过缓存函数引用来解决这个问题。 它返回一个在组件多次渲染间保持稳定的函数,只有当依赖数组中的值变化时才创建新函数。 这使得传递给子组件的函数引用保持稳定,让 React.memo 的浅比较优化生效。

核心机制:useCallback 本质上是 useMemo 的语法糖, 专门用于缓存函数。它在首次渲染时创建并缓存函数,后续渲染时检查依赖数组。 如果依赖未变,返回缓存的函数引用;如果依赖变了,创建新函数并更新缓存。

适用场景:当函数作为 props 传递给优化的子组件、 作为其他 Hook(如 useEffect)的依赖、或作为 useMemo 的计算函数时使用 useCallback。 但不要过度优化——只有性能测试表明有必要时才使用。

💡 心智模型

将 useCallback 想象成"函数名片盒":

  • 首次名片:创建函数并打印名片(引用),存入名片盒
  • 后续使用:有人要名片时,先检查盒子里是否还有名片
  • 检查内容:对比名片上的"依赖内容"是否变化
  • 复用 vs 重印:内容没变就给旧名片,内容变了才重新打印

关键:名片上的"依赖内容"指的是函数内部使用的外部变量(如 props、state)。 只有这些变量变化时,才需要重新创建函数。

技术规格

类型签名

TypeScript
function useCallback<T extends (...args: any[]) => any>(
  callback: T,
  dependencies: DependencyList
): T

// 泛型 T 是被缓存函数的类型
// callback 是要缓存的函数
// dependencies 是依赖数组,只有数组中的值变化时才重新创建函数

参数说明

参数类型说明
callbackT要缓存的函数。这个函数可以接受任何参数并返回任何值。 React 会在首次渲染时返回这个函数本身。
dependenciesDependencyList依赖数组。React 使用 Object.is 比较数组中每个值与上次渲染的值。 只要有一个值变化,就会重新创建并缓存新函数。

返回值

返回值类型说明
缓存函数T首次渲染返回 callback 函数本身。后续渲染中,如果依赖未变, 返回上次缓存的函数引用(相同的内存地址);如果依赖变了,返回新创建的函数。

运行机制

等价实现:useCallback 实际上是 useMemo 的语法糖:

TypeScript
// 这两个完全等价:
const cachedFn = useCallback(() => doSomething(a, b), [a, b]);

const cachedFn = useMemo(() => () => doSomething(a, b), [a, b]);

引用稳定性:缓存函数在依赖不变时保持同一个引用(内存地址)。 这对于 React.memo 的浅比较很重要——如果对象/函数引用相同,React 认为值没变。

性能权衡:useCallback 本身也有开销——依赖比较和存储缓存。 不要对所有函数都使用,只在确实需要稳定引用的场景使用。

实战演练

1. 基础用法:优化子组件渲染

最常见的场景是配合 React.memo 优化子组件渲染:

TypeScript
import { useCallback, useState, memo } from 'react';

interface PostProps {
  id: number;
  title: string;
  onLike: (id: number) => void;
}

// ✅ 子组件使用 React.memo 优化
const Post = memo(function Post({ id, title, onLike }: PostProps) {
  console.log(`Post ${id} 渲染`); // 观察渲染次数
  return (
    <div className="p-4 border rounded mb-2">
      <h3 className="font-semibold">{title}</h3>
      <button
        onClick={() => onLike(id)}
        className="px-3 py-1 bg-blue-500 text-white rounded"
      >
        点赞
      </button>
    </div>
  );
});

export function PostList({ posts }: { posts: Array<{ id: number; title: string }> }) {
  const [likeCount, setLikeCount] = useState(0);

  // ✅ 使用 useCallback 稳定函数引用
  const handleLike = useCallback((id: number) => {
    setLikeCount(c => c + 1);
  }, []); // 空依赖,函数永不改变

  return (
    <div className="p-6">
      <p className="mb-4">总点赞数: {likeCount}</p>

      {/* 这个按钮会导致 PostList 重新渲染,但 Post 不会重新渲染 */}
      <button
        onClick={() => setLikeCount(likeCount + 1)}
        className="px-4 py-2 mb-4 bg-gray-200 rounded"
      >
        计数: {likeCount}
      </button>

      <div>
        {posts.map(post => (
          <Post
            key={post.id}
            {...post}
            onLike={handleLike} // ✅ 稳定引用,Post 不会重新渲染
          />
        ))}
      </div>
    </div>
  );
}

2. 生产级案例:事件处理器优化

在列表渲染中,使用 useCallback 避免为每个元素创建新的事件处理器:

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

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserListProps {
  users: User[];
  onSelect: (userId: string) => void;
}

export function UserList({ users, onSelect }: UserListProps) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // ✅ 缓存选择函数,避免每次渲染都创建新函数
  const handleSelect = useCallback((userId: string) => {
    setSelectedId(userId);
    onSelect(userId); // 调用父组件传入的回调
  }, [onSelect]); // onSelect 依赖变化时才重新创建

  return (
    <div className="p-6">
      <ul className="space-y-2">
        {users.map(user => (
          <li
            key={user.id}
            onClick={() => handleSelect(user.id)}
            className={`p-3 border rounded cursor-pointer ${
              selectedId === user.id
                ? 'bg-blue-100 border-blue-500'
                : 'bg-white hover:bg-gray-50'
            }`}
          >
            <p className="font-semibold">{user.name}</p>
            <p className="text-sm text-gray-600">{user.email}</p>
          </li>
        ))}
      </ul>
    </div>
  );
}

3. 生产级案例:作为 useEffect 依赖

当函数作为 useEffect 的依赖时,使用 useCallback 确保依赖稳定:

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

interface ChatMessage {
  id: string;
  content: string;
  timestamp: number;
}

export function ChatRoom({ roomId }: { roomId: string }) {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [isConnected, setIsConnected] = useState(false);

  // ✅ 使用 useCallback 缓存消息处理函数
  const handleMessage = useCallback((message: ChatMessage) => {
    setMessages(prev => [...prev, message]);
  }, []);

  // ✅ 使用 useCallback 缓存连接状态更新函数
  const handleConnectionChange = useCallback((connected: boolean) => {
    setIsConnected(connected);
  }, []);

  // ✅ handleMessage 是稳定依赖,effect 不会频繁重新建立
  useEffect(() => {
    console.log('建立 WebSocket 连接...');

    // 模拟 WebSocket 连接
    const ws = {
      send: (data: any) => console.log('发送:', data),
      close: () => console.log('关闭连接'),
      onmessage: (handler: (msg: any) => void) => {
        // 模拟接收消息
        handler({ content: '新消息', id: '1', timestamp: Date.now() });
      },
      onopen: () => handleConnectionChange(true),
      onclose: () => handleConnectionChange(false),
    };

    ws.onmessage(handleMessage);

    return () => {
      ws.close();
    };
  }, [roomId, handleMessage, handleConnectionChange]);

  return (
    <div className="p-6">
      <div className="mb-4">
        状态: {isConnected ? '已连接' : '未连接'}
      </div>
      <ul className="space-y-2">
        {messages.map(msg => (
          <li key={msg.id} className="p-2 bg-gray-100 rounded">
            {msg.content}
          </li>
        ))}
      </ul>
    </div>
  );
}

4. 生产级案例:减少依赖项

使用函数式更新减少依赖,或使用 useReducer 完全消除依赖:

TypeScript
import { useCallback, useState, useReducer } from 'react';

// ❌ 问题:依赖数组很长
function FormWithoutReducer({ user, preferences }: {
  user: { id: string; name: string };
  preferences: { theme: string; language: string };
}) {
  const [status, setStatus] = useState('idle');

  const handleSubmit = useCallback(() => {
    // 使用多个外部变量
    console.log('提交用户:', user.id, user.name);
    console.log('主题:', preferences.theme);
    console.log('语言:', preferences.language);

    setStatus('submitting');
    // 提交逻辑...
  }, [user, preferences, status]); // ❌ 很多依赖,容易变化

  return <button onClick={handleSubmit}>提交</button>;
}

// ✅ 方案 1: 使用函数式更新减少依赖
function FormWithFunctionalUpdate({ userId }: { userId: string }) {
  const [status, setStatus] = useState('idle');

  const handleSubmit = useCallback(() => {
    // ✅ 使用函数式更新,不依赖外部状态
    setStatus(prev => {
      console.log('提交用户:', userId);
      console.log('当前状态:', prev);
      return 'submitting';
    });
  }, [userId]); // ✅ 只依赖 userId

  return <button onClick={handleSubmit}>提交</button>;
}

// ✅ 方案 2: 使用 useReducer 消除依赖
type FormState = { status: 'idle' | 'submitting' | 'success' | 'error' };
type FormAction = { type: 'SUBMIT' } | { type: 'RESET' };

function formReducer(state: FormState, action: FormAction): FormState {
  switch (action.type) {
    case 'SUBMIT':
      return { status: 'submitting' };
    case 'RESET':
      return { status: 'idle' };
    default:
      return state;
  }
}

function FormWithReducer({ user, preferences }: {
  user: { id: string; name: string };
  preferences: { theme: string; language: string };
}) {
  const [state, dispatch] = useReducer(formReducer, { status: 'idle' });

  // ✅ dispatch 引用永远稳定,无依赖!
  const handleSubmit = useCallback(() => {
    console.log('提交用户:', user.id);
    console.log('主题:', preferences.theme);
    dispatch({ type: 'SUBMIT' });
  }, [user, preferences]); // 仍然需要 user 和 preferences

  // 更好的方案:将 user 和 preferences 放入 reducer state
  return <button onClick={handleSubmit}>提交</button>;
}

避坑指南

陷阱 1: 过度使用 useCallback

问题:对所有函数都使用 useCallback,包括只在本地使用的函数。 这会增加代码复杂度,还可能降低性能(依赖比较也有成本)。

TypeScript
// ❌ 过度优化:本地使用的函数不需要 useCallback
function Form() {
  const [value, setValue] = useState('');

  // ❌ 不必要的 useCallback
  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  }, []);

  const handleSubmit = useCallback(() => {
    console.log(value);
  }, [value]);

  return (
    <form>
      <input value={value} onChange={handleChange} />
      <button onClick={handleSubmit}>提交</button>
    </form>
  );
}
TypeScript
// ✅ 正确:直接声明函数
function Form() {
  const [value, setValue] = useState('');

  // ✅ 直接声明,简洁清晰
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const handleSubmit = () => {
    console.log(value);
  };

  return (
    <form>
      <input value={value} onChange={handleChange} />
      <button onClick={handleSubmit}>提交</button>
    </form>
  );
}

经验法则:只在函数传递给 memo 优化的子组件, 或作为 useEffect/useMemo 依赖时才使用 useCallback。

陷阱 2: 函数使用过期的 props 或 state

问题:依赖数组不完整或为空,导致函数内部使用的是旧闭包值。

TypeScript
// ❌ 错误:空依赖导致闭包陷阱
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    // ❌ count 永远是初始值 0!
    console.log('当前计数:', count);
    setCount(count + 1);
  }, []); // 空依赖

  return <button onClick={handleClick}>增加</button>;
}
TypeScript
// ✅ 方案 1: 添加依赖
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('当前计数:', count);
    setCount(count + 1);
  }, [count]); // ✅ 添加 count 依赖

  return <button onClick={handleClick}>增加</button>;
}

// ✅ 方案 2: 使用函数式更新
function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    // ✅ 函数式更新,读取最新值
    setCount(c => {
      const newValue = c + 1;
      console.log('新计数:', newValue);
      return newValue;
    });
  }, []); // ✅ 无依赖,不会闭包陷阱

  return <button onClick={handleClick}>增加</button>;
}

陷阱 3: 依赖包含频繁变化的对象/数组

问题:依赖数组中的对象或数组每次渲染都是新引用, 导致 useCallback 失去缓存效果。

TypeScript
// ❌ 错误:options 对象每次渲染都是新引用
function Component({ items }: { items: number[] }) {
  const [filter, setFilter] = useState({ min: 0, max: 100 });

  const handleProcess = useCallback(() => {
    console.log('最小值:', filter.min);
    console.log('最大值:', filter.max);
    console.log('处理项目:', items);
  }, [filter, items]); // ❌ filter 和 items 每次都是新引用!

  return <button onClick={handleProcess}>处理</button>;
}
TypeScript
// ✅ 方案 1: 只依赖需要的值
function Component({ items }: { items: number[] }) {
  const [filter, setFilter] = useState({ min: 0, max: 100 });

  const handleProcess = useCallback(() => {
    console.log('最小值:', filter.min);
    console.log('最大值:', filter.max);
    // ❌ 仍然依赖 items,如果 items 变化还是会重新创建
  }, [filter.min, filter.max]); // ✅ 只依赖基本类型

  return <button onClick={handleProcess}>处理</button>;
}

// ✅ 方案 2: 使用 useMemo 稳定对象引用
import { useMemo } from 'react';

function Component({ items }: { items: number[] }) {
  const [filter, setFilter] = useState({ min: 0, max: 100 });

  // ✅ 使用 useMemo 稳定 filter 对象
  const stableFilter = useMemo(() => filter, [JSON.stringify(filter)]);

  const handleProcess = useCallback(() => {
    console.log('过滤器:', stableFilter);
    console.log('处理项目:', items);
  }, [stableFilter, items]);

  return <button onClick={handleProcess}>处理</button>;
}

// ✅ 方案 3: 将 items 作为参数传入
function Component() {
  const [items, setItems] = useState<number[]>([]);

  // ✅ items 作为参数,不在依赖数组中
  const handleProcess = useCallback((itemsToProcess: number[]) => {
    console.log('处理项目:', itemsToProcess);
  }, []); // ✅ 无外部依赖

  return <button onClick={() => handleProcess(items)}>处理</button>;
}

陷阱 4: 在 JSX 中创建内联函数

问题:在 JSX 中使用箭头函数或 bind 创建新函数, 即使子组件使用了 useCallback,也会破坏优化。

TypeScript
// ❌ 错误:在 JSX 中创建新函数
function UserList({ users, onSelect }: {
  users: Array<{ id: string; name: string }>;
  onSelect: (id: string) => void;
}) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return (
    <ul>
      {users.map(user => (
        <li
          key={user.id}
          // ❌ 每次渲染都创建新函数!
          onClick={() => onSelect(user.id)}
          // ❌ 或者这样
          onClick={handleSelect.bind(null, user.id)}
          // ❌ 或者这样
          onClick={() => {
            setSelectedId(user.id);
            onSelect(user.id);
          }}
          className={selectedId === user.id ? 'bg-blue-100' : ''}
        >
          {user.name}
        </li>
      ))}
    </ul>
  );
}
TypeScript
// ✅ 正确:使用 useCallback + 数据属性
import { useCallback } from 'react';

function UserList({ users, onSelect }: {
  users: Array<{ id: string; name: string }>;
  onSelect: (id: string) => void;
}) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // ✅ 缓存处理函数
  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
    onSelect(id);
  }, [onSelect]);

  // ✅ 方案 1: 传递 ID,让子组件处理点击
  return (
    <ul>
      {users.map(user => (
        <li
          key={user.id}
          // ❌ 还是创建了新函数包装器!
          // onClick={() => handleSelect(user.id)}

          // ✅ 使用 data 属性
          data-id={user.id}
          onClick={(e) => handleSelect(e.currentTarget.dataset.id || '')}
          className={selectedId === user.id ? 'bg-blue-100' : ''}
        >
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// ✅ 方案 2: 使用子组件
function UserItem({
  user,
  onSelect,
  isSelected
}: {
  user: { id: string; name: string };
  onSelect: (id: string) => void;
  isSelected: boolean;
}) {
  // ✅ 子组件内部处理点击,不需要 useCallback
  return (
    <li
      onClick={() => onSelect(user.id)}
      className={isSelected ? 'bg-blue-100' : ''}
    >
      {user.name}
    </li>
  );
}

function UserList({ users, onSelect }: {
  users: Array<{ id: string; name: string }>;
  onSelect: (id: string) => void;
}) {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
    onSelect(id);
  }, [onSelect]);

  return (
    <ul>
      {users.map(user => (
        <UserItem
          key={user.id}
          user={user}
          onSelect={handleSelect}
          isSelected={selectedId === user.id}
        />
      ))}
    </ul>
  );
}

最佳实践

✅ 推荐模式

  • 函数传递给 memo 优化子组件时使用
  • 函数作为 useEffect/useMemo 依赖时使用
  • 使用 ESLint exhaustive-deps 规则检查依赖
  • 优先使用函数式更新减少依赖
  • 考虑使用 useReducer 消除函数依赖
  • 在列表渲染中使用子组件而非内联函数

❌ 避免模式

  • 不要对本地使用的函数使用 useCallback
  • 不要忽略依赖数组的警告
  • 不要依赖频繁变化的对象/数组引用
  • 不要在 JSX 中使用内联箭头函数
  • 不要为了"最佳实践"而无脑使用
  • 不要在 useCallback 中执行副作用

📊 useCallback vs useMemo vs 直接声明

场景直接声明useCallbackuseMemo
本地事件处理✅ 推荐❌ 过度❌ 错误
传递给 memo 子组件⚠️ 可能失效✅ 推荐⚠️ 间接实现
作为 useEffect 依赖⚠️ 不稳定✅ 推荐✅ 等价
缓存计算结果❌ 不适用❌ 错误✅ 推荐

延伸阅读