useCallback:从性能焦虑到精准优化的轻松之路

大家好,我是小杨。不知道你有没有过这种"性能焦虑"?在编写 React 函数组件时,每当看到 eslint 提示 'xxx' function makes the dependencies of useEffect Hook change on every render,心头就一紧。

我曾经也是这样,直到我真正理解了 useCallback 的正确打开方式。今天,我们就来聊聊它,我会用最直白的方式,帮你告别无效优化,实现精准性能提升。

useCallback 到底是什么?简单饭局说清楚

你可以把 useCallback 想象成一个"记忆大师"。它的工作就是在依赖项不变的情况下,返回同一个函数引用,而不是每次渲染都创建一个新的函数。

这有什么用?我举个生活中的例子:

假设你(组件)每天都要告诉你的朋友(子组件)一个秘密(函数)。如果这个秘密每天都不一样,朋友就得每天重新记一遍(子组件重新渲染)。但如果这个秘密好几天都一样,你只需要说"还是昨天那个秘密"(相同的函数引用),朋友就不用费脑子再记了(避免不必要的渲染)。

在 React 中,useCallback 就是帮你做这个事情的。它"记住"了一个函数,只有在它的依赖项数组发生变化时,它才会"翻脸"创建一个新的。

一个真实场景:避免子组件不必要的渲染

这是 useCallback 最经典的使用场景。我们来看一段代码。

假设我有一个父组件,它包含一个昂贵的子组件 ExpensiveChildComponent,以及一个计数器:

没有 useCallback 时的问题代码:

jsx 复制代码
import React, { useState } from 'react';

// 一个假设非常"昂贵"的子组件,用了 React.memo 进行优化
const ExpensiveChild = React.memo(({ onButtonClick }) => {
  console.log('子组件被渲染了!'); // 如果控制台频繁打印这个,说明性能有问题
  return <button onClick={onButtonClick}>点击我</button>;
});

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

  // 问题在这里:每次 ParentComponent 渲染,都会创建一个全新的 handleClick 函数
  const handleClick = () => {
    console.log('按钮被点击了!', name); // 这里用到了 state `name`
  };

  return (
    <div>
      <input 
        type="text" 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
      <p>计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加计数</button>
      
      {/* 即使子组件用了 memo,因为每次 handleClick 都是新的,子组件还是会重新渲染 */}
      <ExpensiveChild onButtonClick={handleClick} />
    </div>
  );
}

发生了什么?

当我点击"增加计数"按钮时,count 状态改变,导致 ParentComponent 重新渲染。这会重新创建 handleClick 函数。虽然 ExpensiveChild 用了 React.memo 进行保护,但它接收的 onButtonClick prop 每次都是一个新的函数引用,所以 React.memo 的对比失败了,子组件被迫跟着重新渲染。这完全浪费了 memo 的优化效果!

用 useCallback 进行优化:

jsx 复制代码
import React, { useState, useCallback } from 'react';

const ExpensiveChild = React.memo(({ onButtonClick }) => {
  console.log('子组件被渲染了!'); // 现在只有 name 改变时,这里才会打印
  return <button onClick={onButtonClick}>点击我</button>;
});

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

  // 使用 useCallback 将函数缓存起来
  // 依赖项数组 [name] 表示:只有当 name 改变时,才会重新创建 handleClick
  const handleClick = useCallback(() => {
    console.log('按钮被点击了!', name);
  }, [name]); // ✅ 依赖项必须写全!

  return (
    <div>
      <input 
        type="text" 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
      />
      <p>计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加计数</button>
      
      {/* 现在,点击"增加计数"按钮时,handleClick 的引用不变,子组件不会重新渲染! */}
      <ExpensiveChild onButtonClick={handleClick} />
    </div>
  );
}

优化后的效果:

现在,当我点击"增加计数"按钮时,虽然父组件重新渲染了,但 useCallback 发现它的依赖项 [name] 没有变化,所以它会直接返回上一次记忆的 handleClick 函数。ExpensiveChild 接收到的 onButtonClick prop 引用没有变化,React.memo 的对比成功,阻止了子组件不必要的渲染!只有当我修改 input(改变了 name)时,handleClick 才会被重新创建,并触发子组件重新渲染。

useCallback 的黄金搭档:useMemo 和 React.memo

  • React.memo: 用于优化子组件,对 props 进行浅比较,避免不必要的重渲染。
  • useCallback : 用于稳定作为 props 传递的函数 ,辅助 React.memo 发挥作用。
  • useMemo : 用于稳定作为 props 传递的复杂计算值 (比如计算数组、对象),同样是辅助 React.memo

它们三个常常一起使用,构成性能优化的"铁三角"。

什么时候不该用 useCallback?

别急着到处用!滥用 useCallback 反而会增加性能开销(函数本身需要被记忆,依赖项要进行比较)。遵循这个原则:

只在以下情况使用它:

  1. 函数被作为 prop 传递给被 React.memo 优化的子组件。
  2. 函数是其他 Hook(如 useEffect)的依赖项。
  3. 函数被用于上下文(Context)或其他内部需要稳定引出的地方。

如果你的函数只是在一个普通的、没有性能优化需求的组件内部使用,直接定义它就好了,完全不需要 useCallback

总结

useCallback 不是银弹,它是一把精准的手术刀。它的核心价值在于通过稳定函数引用,来保证依赖此函数的其他优化(如 React.memo, useEffect)能够生效

记住我们的优化路径:发现性能问题 -> 用 React.memo 包裹子组件 -> 再用 useCallbackuseMemo 稳定那些会导致 memo 失效的 props

希望这篇文章能帮你打消对 useCallback 的困惑,从此告别盲目优化,走向精准高效!如果你有其他有趣的使用场景或问题,欢迎在评论区一起讨论。

⭐ 写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

相关推荐
wycode6 分钟前
# 面试复盘(2)--某硬件大厂前端
前端·面试
怪可爱的地球人7 分钟前
ts枚举(enum)
前端
做你的猫10 分钟前
深入剖析:基于Vue 3与Three.js的3D知识图谱实现与优化
前端·javascript·vue.js
渊不语14 分钟前
富文本编辑器自定义图片等工具栏-完整开发文档
前端
用户239712822487015 分钟前
taro+vue3+vite项目 tailwind 踩坑记,附修复后的模板源码地址
前端
做你的猫19 分钟前
深入剖析:基于Vue 3的高性能AI聊天组件设计与实现
前端·javascript·vue.js
G佳伟21 分钟前
vue拖动排序,vue使用 HTML5 的draggable拖放 API实现内容拖并排序,并更新数组数据
前端·vue.js·html5
Bling_Bling_126 分钟前
ES6新语法特性(第二篇)
开发语言·前端·es6
石小石Orz38 分钟前
妙啊!Js的对象属性居然还能用这么写
前端
成熟的API调用专家44 分钟前
cesium 获取鼠标点击位置的经度纬度海拔高度
前端