学习如何驯服 React 的 useCallback Hook
已发表: 2022-04-27React.js 近年来广受欢迎,这已经不是什么秘密了。 它现在是许多互联网上最杰出的参与者(包括 Facebook 和 WhatsApp)的首选 JavaScript 库。
它兴起的主要原因之一是在 16.8 版本中引入了钩子。 React 钩子允许您在不编写类组件的情况下利用 React 功能。 现在,带有钩子的功能组件已成为开发人员使用 React 的首选结构。
在这篇博文中,我们将深入研究一个特定的钩子useCallback
因为它涉及函数式编程的一个基本部分,即记忆化。 您将确切地知道如何以及何时使用useCallback
挂钩并充分利用其性能增强功能。
准备好? 让我们潜入吧!
什么是记忆?
记忆是当一个复杂的函数存储它的输出以便下次使用相同的输入调用它时。 它类似于缓存,但在本地级别上。 它可以跳过任何复杂的计算并更快地返回输出,因为它已经计算过了。
这会对内存分配和性能产生重大影响,而这种压力正是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])
上面的代码片段是一个人为的示例,但显示了两个回调之间的区别:
-
memoizedValue
将成为数组[1, 2, 3, 4, 6, 9]
。 只要 values 变量保持不变,memoizedValue
也会保持不变,并且它永远不会重新计算。 -
memoizedFunction
将是一个返回数组[1, 2, 3, 4, 6, 9]
的函数。
这两个回调的好处是它们会被缓存并一直存在,直到依赖数组发生变化。 这意味着在渲染时,它们不会被垃圾收集。
特渲染和反应
为什么在 React 中记忆很重要?
它与 React 如何渲染你的组件有关。 React 使用存储在内存中的虚拟 DOM 来比较数据并决定更新什么。
虚拟 DOM 帮助 React 提高性能并让您的应用程序保持快速。 默认情况下,如果您的组件中的任何值发生更改,整个组件将重新渲染。 这使得 React 对用户输入具有“反应性”,并允许屏幕更新而无需重新加载页面。
您不想渲染组件,因为更改不会影响该组件。 这就是通过useCallback
和useMemo
进行记忆的地方。
当 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
更有意义:
- 您正在将该函数传递给另一个也被记忆的组件(
useMemo
) - 你的函数有一个需要记住的内部状态
- 您的函数是另一个钩子的依赖项,例如
useEffect
React useCallback 的性能优势
当useCallback
被适当地使用时,它可以帮助加速你的应用程序并防止组件在不需要时重新渲染。
例如,假设您有一个组件,它获取大量数据并负责以图表或图形的形式显示该数据,如下所示:
假设您的数据可视化组件的父组件重新渲染,但更改的道具或状态不会影响该组件。 在这种情况下,您可能不想或不需要重新渲染它并重新获取所有数据。 避免这种重新渲染和重新获取可以节省用户的带宽并提供更流畅的用户体验。
React useCallback 的缺点
虽然这个钩子可以帮助你提高性能,但它也有它的缺陷。 在使用useCallback
(和useMemo
)之前需要考虑的一些事情是:
- 垃圾收集: React 将丢弃其他尚未记忆的函数以释放内存。
- 内存分配:与垃圾回收类似,你拥有的记忆功能越多,需要的内存就越多。 另外,每次你使用这些回调时,React 中都有一堆代码需要使用更多的内存来为你提供缓存的输出。
- 代码复杂性:当您开始在这些钩子中包装函数时,您会立即增加代码的复杂性。 现在需要更多地了解为什么使用这些钩子并确认它们被正确使用。
意识到上述陷阱可以避免你自己绊倒它们的头痛。 在考虑使用useCallback
时,请确保性能优势将超过缺点。
反应使用回调示例
下面是一个带有 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 视为同一个函数,直到依赖数组中的某个项目的值发生变化(例如countOne
、 countTwo
)。
概括
和useCallback
和useMemo
一样酷,记住它们有特定的用例——你不应该用这些钩子来包装每个函数。 如果函数计算复杂,则另一个钩子的依赖关系或传递给记忆化组件的道具是您可能想要达到的好指标useCallback
。
我们希望这篇文章能帮助你理解这个高级的 React 功能,并帮助你在使用函数式编程的过程中获得更多的信心!