学习如何驯服React的useCallback钩子

React.js近年来广受欢迎,这已经不是什么秘密了。它现在是许多互联网上最杰出的参与者(包括Facebook和WhatsApp)的首选JavaScript库。

它兴起的主要原因之一是在16.8版本中引入了钩子。React钩子允许您在不编写类组件的情况下利用React函数。现在,带有钩子的功能组件已成为开发人员使用React的首选结构。

在这篇博文中,我们将深入研究一个特定的钩子useCallback——因为它涉及函数式编程的一个基本部分,即记忆化。您将确切地知道如何以及何时使用useCallback钩子并充分利用其性能增强功能。

  1. 什么是Memoization?
  2. 渲染和反应
  3. React useCallback的性能优势
  4. React useCallback的缺点
  5. React使用回调示例

什么是Memoization?

Memoization是当一个复杂的函数存储它的输出以便下次使用相同的输入调用它时。它类似于缓存,但在本地级别上。它可以跳过任何复杂的计算并更快地返回输出,因为它已经计算过了。

这会对内存分配和性能产生重大影响,而这种压力正是useCallback钩子的目的所在。

React的useCallback与useMemo

在这一点上,值得一提的是,useCallback与另一个名为useMemo的钩子很好地配对。我们将讨论它们,但在这篇文章中,我们将把重点放在useCallback主要话题上。

关键区别在于useMemo返回一个记忆值,而useCallback返回一个记忆函数。这意味着useMemo用于存储计算值,同时useCallback返回一个您可以稍后调用的函数。

这些钩子会给你一个缓存的版本,除非它们的依赖项之一(例如状态或道具)发生变化。

让我们看一下这两个函数的作用:

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]
// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])
// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])

上面的代码片段是一个人为的示例,但显示了两个回调之间的区别:

  1. memoizedValue会变成数组[1, 2, 3, 4, 6, 9]。只要values变量保持不变,memoizedValue它就会保持不变,并且永远不会重新计算。
  2. memoizedFunction将是一个返回数组的函数[1, 2, 3, 4, 6, 9]

这两个回调的好处是它们会被缓存并一直存在,直到依赖数组发生变化。这意味着在渲染时,它们不会被垃圾收集。

渲染和React

为什么在React中记忆很重要?

它与React如何渲染你的组件有关。React使用存储在内存中的虚拟DOM来比较数据并决定更新什么。

虚拟DOM帮助React提高性能并让您的应用程序保持快速。默认情况下,如果您的组件中的任何值发生更改,整个组件将重新渲染。这使得React对用户输入具有“反应性”,并允许屏幕更新而无需重新加载页面。

您不想渲染组件,因为更改不会影响该组件。这就是通过useCallbackuseMemo进行记忆的地方。

当React重新渲染你的组件时,它也会重新创建你在组件中声明的函数。

请注意,当比较一个函数与另一个函数的相等性时,它们总是为假的。因为函数也是一个对象,所以它只会等于它自己:

// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')
hello === hello2 // false
hello === hello // true

换句话说,当React重新渲染你的组件时,它会将在你的组件中声明的任何函数都视为新函数。

这在大多数情况下都很好,简单的函数很容易计算并且不会影响性能。但是其他时候,当您不希望该功能被视为新功能时,您可以依靠useCallback来帮助您。

你可能会想,“我什么时候不希望一个函数被视为一个新函数?” 好吧,在某些情况下useCallback更有意义:

  1. 您将函数传递给另一个也被记忆的组件(useMemo
  2. 你的函数有一个需要记住的内部状态
  3. 您的函数是另一个钩子的依赖项,例如useEffect

React useCallback的性能优势

如果useCallback使用得当,它可以帮助加速您的应用程序并防止组件在不需要时重新渲染。

例如,假设您有一个组件,它获取大量数据并负责以图表或图形的形式显示该数据,如下所示:

 

使用React组件生成的条形图

使用React组件生成的条形图

 

假设您的数据可视化组件的父组件重新渲染,但更改的道具或状态不会影响该组件。在这种情况下,您可能不想或不需要重新渲染它并重新获取所有数据。避免这种重新渲染和重新获取可以节省用户的带宽并提供更流畅的用户体验。

React useCallback的缺点

虽然这个钩子可以帮助你提高性能,但它也有它的缺陷。在使用useCallback(和useMemo)之前,需要考虑以下几点:

  • 垃圾收集: React将丢弃其他尚未记忆的函数以释放内存。
  • 内存分配:与垃圾回收类似,你拥有的记忆功能越多,需要的内存就越多。另外,每次你使用这些回调时,React中都有一堆代码需要使用更多的内存来为你提供缓存的输出。
  • 代码复杂性:当您开始在这些钩子中包装函数时,您会立即增加代码的复杂性。现在需要更多地了解为什么使用这些钩子并确认它们被正确使用。

意识到以上这些陷阱可以让你省去自己跌跌撞撞的头痛。在考虑使用useCallback时,请确保性能优势大于缺点。

React使用回调示例

下面是一个带有Button组件和Counter组件的简单设置。Counter有两个状态并渲染出两个Button组件,每个组件将更新Counter组件状态的一个单独部分。

Button组件有两个props:handleClick和name。每次呈现Button时,它都会登录到控制台。

import { useCallback, useState } from 'react'
const Button = ({handleClick, name}) => {
console.log(`${name} rendered`)
return <button onClick={handleClick}>{name}</button>
}
const Counter = () => {
console.log('counter rendered')
const [countOne, setCountOne] = useState(0)
const [countTwo, setCountTwo] = useState(0)
return (
<>
{countOne} {countTwo}
<Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
<Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
</>
)
}

在此示例中,无论何时单击任一按钮,您都会在控制台中看到:

// counter rendered
// button1 rendered
// button2 rendered

现在,如果我们应用useCallback到我们的handleClick函数并将我们的Button包装在 中React.memo,我们可以看到useCallback为我们提供了什么。React.memo类似于useMemo并且允许我们记忆一个组件。

import { useCallback, useState } from 'react'
const Button = React.memo(({handleClick, name}) => {
console.log(`${name} rendered`)
return <button onClick={handleClick}>{name}</button>
})
const Counter = () => {
console.log('counter rendered')
const [countOne, setCountOne] = useState(0)
const [countTwo, setCountTwo] = useState(0)
const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
return (
<>
{countOne} {countTwo}
<Button handleClick={memoizedSetCountOne} name="button1" />
<Button handleClick={memoizedSetCountTwo} name="button1" />
</>
)
}

现在,当我们单击任一按钮时,我们只会看到我们单击以登录控制台的按钮:

// counter rendered
// button1 rendered
// counter rendered
// button2 rendered

我们已经将memoization应用于我们的按钮组件,并且传递给它的prop值被视为相等。这两个handleClick函数被缓存并且将被React视为同一个函数,直到依赖数组中的项目的值发生变化(例如countOnecountTwo)。

小结

尽管useCallbackuseMemo很酷,但请记住它们有特定的用例——您不应该用这些挂钩包装每个函数。如果函数在计算上很复杂,那么传递给记忆组件的另一个钩子或道具的依赖关系是很好的指标,您可能希望获得useCallback

我们希望这篇文章能帮助你理解这个高级的React功能,并帮助你在使用函数式编程的过程中获得更多的信心!

本文提到: