react中的useCallback

useCallback 是 React 中一个重要的性能优化 Hook,用于缓存函数引用 ,避免在组件重新渲染时创建新的函数实例。它在配合 React.memo、子组件优化、事件处理函数传递等场景中非常有用。


一、基本原理

1. 函数是引用类型

在 JavaScript 中,每次定义函数(即使是内容完全相同的函数),都会创建一个新的引用:

ini 复制代码
js
编辑
const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false

在 React 函数组件中,每次渲染都会重新执行整个函数体,因此其中定义的函数也会被重新创建。

2. 问题:不必要的子组件重渲染

当父组件将内联函数作为 prop 传给子组件(尤其是用 React.memo 包裹的子组件)时,由于函数引用变化,即使 props 内容没变,子组件也会重新渲染。

3. 解决方案:useCallback

useCallback 通过依赖数组(deps)缓存函数引用,只有在依赖项变化时才返回新函数,否则返回上一次缓存的函数。

ini 复制代码
js
编辑
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b]
);

其内部实现逻辑大致如下(简化版):

ini 复制代码
js
编辑
function useCallback(callback, deps) {
  const hook = getHook(); // 获取当前 hook 状态
  if (depsChanged(hook.deps, deps)) {
    hook.callback = callback;
    hook.deps = deps;
  }
  return hook.callback;
}

注意:useCallback(fn, deps) 等价于 useMemo(() => fn, deps)


二、详细用法

1. 基本用法:缓存事件处理函数

javascript 复制代码
jsx
编辑
import { useState, useCallback } from 'react';

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

  // 缓存 handleClick,仅当 count 变化时才更新
  const handleClick = useCallback(() => {
    console.log('Clicked!', count);
  }, [count]);

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <button onClick={() => setCount(c => c + 1)}>+</button>
      {/* 传递缓存后的函数 */}
      <Child onClick={handleClick} />
    </div>
  );
}

// 使用 React.memo 避免无意义重渲染
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

如果没有 useCallback,每次输入 name 都会导致 handleClick 引用变化,从而触发 Child 重渲染。


2. 与自定义 Hook 结合使用

ini 复制代码
js
编辑
function useApiCall(url) {
  const fetch = useCallback(async () => {
    const res = await fetch(url);
    return res.json();
  }, [url]);

  return fetch;
}

确保在 url 不变时,fetch 函数引用不变,便于其他组件依赖它。


3. 用于高阶函数或回调链

scss 复制代码
js
编辑
const handleSave = useCallback((data) => {
  api.save(data).then(onSuccess);
}, [onSuccess]); // onSuccess 本身也应是稳定引用

注意:如果 onSuccess 是父组件传入的函数,也建议用 useCallback 包裹,否则会导致 handleSave 频繁变化。


4. 与 useEffect 配合(作为依赖)

scss 复制代码
js
编辑
const fetchData = useCallback(() => {
  // ...
}, [userId]);

useEffect(() => {
  fetchData();
}, [fetchData]); // 安全地作为依赖

如果不使用 useCallbackfetchData 每次渲染都不同,会导致 useEffect 无限循环或频繁执行。


三、注意事项(重要!)

✅ 1. 不要滥用

  • useCallback 本身有轻微性能开销(比较依赖、存储缓存)。
  • 如果函数不作为 prop 传给 React.memo 子组件,或不在 useEffect/useMemo 中作为依赖,通常不需要useCallback
  • 过度使用会增加代码复杂度,得不偿失。

经验法则 :只有当函数被用作依赖项传递给优化过的子组件 时,才考虑 useCallback


✅ 2. 依赖数组必须完整且正确

  • 必须包含函数体内用到的所有响应式值(state、props、其他 hooks 返回值等)。
  • 否则可能导致闭包陷阱(stale closure)------函数使用的是旧值。

❌ 错误示例:

scss 复制代码
js
编辑
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
  console.log(count); // 依赖 count
}, []); // ❌ 缺少依赖

✅ 正确:

ini 复制代码
js
编辑
const handleClick = useCallback(() => {
  console.log(count);
}, [count]); // ✅

✅ 3. 函数参数不影响缓存

useCallback 缓存的是函数引用,不是调用结果。即使你传不同参数调用,函数本身仍是同一个引用(只要依赖没变)。


✅ 4. 箭头函数 vs 命名函数

两种写法都可以,但要注意依赖:

javascript 复制代码
js
编辑
// 写法1:箭头函数
const fn = useCallback((x) => x + a, [a]);

// 写法2:命名函数
const fn = useCallback(function add(x) {
  return x + a;
}, [a]);

效果相同。


✅ 5. 与 useRef 的区别

  • useRef 可以保存可变值,但不会触发重渲染。
  • useCallback 是为了保持函数引用稳定,常用于优化。
  • 有时可用 useRef 保存函数来绕过依赖问题,但会失去响应性,一般不推荐。

✅ 6. 开发模式下可能"失效"

React 在 Strict Mode 下会故意双调用某些函数(包括 useCallback 的回调),这是为了帮助发现副作用,不影响生产行为


四、常见误区

误区 说明
"所有函数都要用 useCallback" ❌ 只有需要稳定引用时才用
"useCallback 能提升函数执行速度" ❌ 它只缓存引用,不影响执行性能
"空依赖数组总是安全的" ❌ 如果函数用了外部变量,必须加入依赖

五、替代方案

  • 如果子组件很小,不值得用 React.memo + useCallback,直接让其重渲染更简单。
  • 对于复杂状态逻辑,考虑用 useReducer,dispatch 函数是稳定的,无需 useCallback
  • 使用 useEvent(实验性 Hook,未来可能加入 React)可自动处理稳定回调,无需手动管理依赖。

总结

useCallback 的核心价值是:在需要函数引用稳定的场景下,避免不必要的重渲染或重复订阅

✅ 正确使用场景:

  • 传递给 React.memo 子组件的函数 props
  • 作为 useEffectuseMemouseCallback 的依赖
  • 传递给第三方库(如事件监听器、动画回调等)需稳定引用的函数

🚫 避免:

  • 为"看起来高级"而加
  • 忽略依赖项
  • 在简单组件中过度优化

合理使用 useCallback,能让你的 React 应用既高效又可维护。

相关推荐
用户8168694747252 小时前
Fiber 双缓存架构与 Diff 算法
前端·react.js
AAA简单玩转程序设计2 小时前
Java集合“坑王”:ArrayList为啥越界还能浪?
java·前端
AAA简单玩转程序设计2 小时前
别再把Java枚举当“花瓶”!它能办大事
java·前端
水冗水孚2 小时前
通俗易懂地谈谈,前端工程化之自定义脚手架的理解,并附上一个实践案例发布到npm上
javascript·npm·node.js
knight_l2 小时前
【附源码,附两款可视化大屏】Three.js中的地图精确贴图与热力图实现解析
前端
华洛2 小时前
《回顾我的AI学习之路,上万字的AI学习思维导图分享》
前端·后端·产品经理
PBitW2 小时前
工作两年,从自己造轮子,变成使用开源项目!
前端·开源·若依·为什么使用开源项目·不自己造轮子
cindershade2 小时前
Vue3 实时音频录制与转写 Composable 技术实现
前端
张风捷特烈3 小时前
Flutter&TolyUI#12 | 树形组件 toly_tree 重磅推出!
android·前端·flutter