如何让你的函数式 React 组件表现更好(使用 useCallback 和 memo)

2025-06-07

如何让你的函数式 React 组件表现更好(使用 useCallback 和 memo)

在使用了 AngularJs 和 Angular 2+ 四年之后,我最近开始接触 React。我非常喜欢这个库开放的本质,以及在模式和实现细节上可以灵活调整。

当你做出很多选择时,这些选择可能会导致大量的错误,作为开发人员,我们需要做更多的工作来确保我们尽最大努力优化我们的 Web 应用程序并决定正确的模式。

在这篇文章中,我将介绍一些我学到的可能对您有用的优化技术。

这是我的第一篇与 React 相关的文章,希望你会喜欢它。

React Hooks

React Hooks 是 React 16.8 中的新增功能。它让你能够在不使用 JavaScript 类的情况下使用状态。这些工具非常强大且易于使用。我不会在这里介绍 Hooks 的基础知识,你可以使用官方 API 参考自行学习,但我将在演示应用程序中使用它们。

演示应用程序

为了这篇文章,我创建了一个简单的 React 应用程序,我们将对其进行一些修改以提升其性能。它小巧简洁,但足以描述我们接下来 5 分钟内要解决的问题。

它是一个数字选择组件,您可以在这里看到它并探索代码:

我们有两个组件。NumPad
组件表示一个带有数字的按钮。它接收一个 props,一个(要显示的数字)、一个handleClick回调函数(用于处理点击事件)和一个布尔值isSelected,用于指示按钮是否应该被选中。如果按钮的isSelected值为正,它将显示绿色以指示选中状态。

第二个也是最大的组件是NumberSelection。该组件具有状态逻辑,并使用useState钩子处理selectedValue变量。NumberSelection在循环中渲染 10按钮,其中一个等于selectedValue的按钮会获得isSelected类。它还表示标题元素中的选定值。初始selectedValue等于 5。

到现在为止还容易吗?

现在让我们深入研究一下该代码存在的问题。

功能组件

在 2019 年,函数式组件被认为是比类组件更好的实践。借助钩子,它们现在允许创建有状态的逻辑。

关于功能组件,我们需要记住一件重要的事情 -它们是在每次渲染时运行的函数,这意味着它们内部的每个东西都会被调用,并且每个变量或函数都会再次声明。

NumberSelection内部有一个名为changeSelection的函数。每次组件状态发生变化时,组件都会被渲染,并且该函数会被反复声明。为了更直观地展示这一点,我修改了原始代码,并添加了一个名为functionsSet的集合。每次组件渲染时,我都会将changeSelection添加到该集合中。


如你所见,每次我们更改选择时,都会创建一个新的函数引用并将其添加到集合中。这无疑不利于内存利用,在某些情况下甚至会导致内存泄漏。但更大的问题是,这个函数通过 props 传递给子组件。这意味着由于内部传递的 props 发生变化,子组件也会重新渲染。

这里有个叫做useCallback的钩子可以帮助我们。它将创建一个函数的记忆版本,当组件重新渲染时,该版本将被重用。

让我们使用useCallback钩子重写changeSelection声明 查看更新的 codepen 示例:


我们不再重新申报它们了。太棒了!

这里还有一点需要记住——很多情况下,没有必要将无状态函数保留在函数式组件内部。如果你的函数是完全静态的——可以考虑将其放在外部,将其移到 utils 库中,或者放在组件声明之前。这样即使不使用useCallback ,也不会重新声明它。

我们真正渲染的是什么?

有很多很棒的文章深入解释了 React 应用程序中渲染的整体工作原理。我不会在这里深入探讨,因为这会占用太多时间。我假设你已经了解 React Virtual Dom 是什么以及它是如何工作的。

我们的应用程序是如何渲染的?当我们改变选择项并且NumberSelection的状态更新时会发生什么?

  1. 整个应用程序首次呈现。
  2. 我们点击NumPad组件所代表的按钮
  3. 这将调用从NumberSelection组件接收的回调函数
  4. NumberSelection组件的状态由于回调而改变。
  5. 一旦状态改变 - 使用该状态的组件和子组件树将在虚拟 DOM 中重建。
  6. 一旦虚拟 DOM 准备就绪,差异算法就会决定应该更新真实 DOM 的哪些部分。假设我们更改了选择,它会更新两个数字键盘组件:一个被取消选择,另一个被选中。

理解起来真好。我添加了一个在NumPad渲染
时写入的新日志。

如您所见,数字键盘会在父组件每次状态改变时重新渲染——这意味着每次点击时,我们都会重新渲染所有 10 个按钮。这是针对虚拟 DOM 进行的渲染——组件实际上不会在真实 DOM 中更新,但我们仍然会调用整个渲染过程。这会运行大量代码。我们真的需要这样做吗?如果我们有 100 个按钮,甚至 1000 个按钮怎么办?

每次选择发生变化时,实际上只有两个数字键盘组件会发生变化:一个是之前被选中的组件(现在会重新被选中),另一个是之前被取消选中的组件。我们实际上不需要重新渲染所有 10 个数字键盘

我们如何知道一个组件是否应该渲染?假设组件是纯函数,我们只需查看传入的 props 即可。如果 props 发生了变化,就表明我们需要渲染该组件。如果没有变化,则无需渲染。

我们应该考虑在这里使用React.memo。它完全满足我们的需要。

React API 规定:
如果你的函数组件在给定相同 props 的情况下渲染相同的结果,你可以将其包装在对 React.memo 的调用中,以便在某些情况下通过记忆结果来提升性能。这意味着 React 将跳过渲染组件,并重用上次渲染的结果。

听起来很像我们的例子!让我们用 React.memo包装一下NumPad :

现在我们看到只有两个相关的组件被渲染了。再次取得巨大成功!

这里值得一提的是,如果不使用上例中的useCallback hook,这个优化将无法正常工作。因为如果没有useCallback hook,每次都会生成一个新函数并将其传递给所有组件,这意味着 React.memo 会检测到 prop 值的变化并渲染组件。

如果React.memo对我们如此有用,为什么 React 不能默认包装所有组件?

替代文本

请记住,不应默认使用memouseCallback 。请检查具体的引用,并分别考虑每种情况,因为误解它们的使用方式可能会导致代码中出现副作用和错误。

希望你玩得开心!
很高兴收到你对我帖子的反馈。

请查看我之前关于 HTML5 中有趣功能的帖子。

在 Twitter 上关注我以获取最新动态!

再见

文章来源:https://dev.to/shadowwarior5/how-to-make-your-function-react-components-perform-b​​etter-using-usecallback-and-memo-2f9a
PREV
根据用户系统设置将您的 Web 应用设置为暗/亮模式
NEXT
旋转轮盘——一个可定制的轮盘,大小不到 30kb,无需 JavaScript 后备。