React.js近年来广受欢迎,这已经不是什么秘密了。它现在是许多互联网上最杰出的参与者(包括Facebook和WhatsApp)的首选JavaScript库。
它兴起的主要原因之一是在16.8版本中引入了钩子。React钩子允许您在不编写类组件的情况下利用React函数。现在,带有钩子的功能组件已成为开发人员使用React的首选结构。
在这篇博文中,我们将深入研究一个特定的钩子useCallback
——因为它涉及函数式编程的一个基本部分,即记忆化。您将确切地知道如何以及何时使用useCallback
钩子并充分利用其性能增强功能。
什么是Memoization?
Memoization是当一个复杂的函数存储它的输出以便下次使用相同的输入调用它时。它类似于缓存,但在本地级别上。它可以跳过任何复杂的计算并更快地返回输出,因为它已经计算过了。
这会对内存分配和性能产生重大影响,而这种压力正是useCallback
钩子的目的所在。
React的useCallback与useMemo
在这一点上,值得一提的是,useCallback
与另一个名为useMemo
的钩子很好地配对。我们将讨论它们,但在这篇文章中,我们将把重点放在useCallback
主要话题上。
关键区别在于useMemo
返回一个记忆值,而useCallback
返回一个记忆函数。这意味着useMemo
用于存储计算值,同时useCallback
返回一个您可以稍后调用的函数。
这些钩子会给你一个缓存的版本,除非它们的依赖项之一(例如状态或道具)发生变化。
让我们看一下这两个函数的作用:
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])
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如何渲染你的组件有关。React使用存储在内存中的虚拟DOM来比较数据并决定更新什么。
虚拟DOM帮助React提高性能并让您的应用程序保持快速。默认情况下,如果您的组件中的任何值发生更改,整个组件将重新渲染。这使得React对用户输入具有“反应性”,并允许屏幕更新而无需重新加载页面。
您不想渲染组件,因为更改不会影响该组件。这就是通过useCallback
和useMemo
进行记忆的地方。
当React重新渲染你的组件时,它也会重新创建你在组件中声明的函数。
请注意,当比较一个函数与另一个函数的相等性时,它们总是为假的。因为函数也是一个对象,所以它只会等于它自己:
const hello = () => console.log(‘Hello Matt’)
const hello2 = () => console.log(‘Hello Matt’)
hello === hello2 // false
hello === hello // true
// 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组件生成的条形图
假设您的数据可视化组件的父组件重新渲染,但更改的道具或状态不会影响该组件。在这种情况下,您可能不想或不需要重新渲染它并重新获取所有数据。避免这种重新渲染和重新获取可以节省用户的带宽并提供更流畅的用户体验。
React useCallback的缺点
虽然这个钩子可以帮助你提高性能,但它也有它的缺陷。在使用useCallback
(和useMemo
)之前,需要考虑以下几点:
- 垃圾收集: React将丢弃其他尚未记忆的函数以释放内存。
- 内存分配:与垃圾回收类似,你拥有的记忆功能越多,需要的内存就越多。另外,每次你使用这些回调时,React中都有一堆代码需要使用更多的内存来为你提供缓存的输出。
- 代码复杂性:当您开始在这些钩子中包装函数时,您会立即增加代码的复杂性。现在需要更多地了解为什么使用这些钩子并确认它们被正确使用。
意识到以上这些陷阱可以让你省去自己跌跌撞撞的头痛。在考虑使用useCallback
时,请确保性能优势大于缺点。
React使用回调示例
下面是一个带有Button组件和Counter组件的简单设置。Counter有两个状态并渲染出两个Button组件,每个组件将更新Counter组件状态的一个单独部分。
Button组件有两个props:handleClick
和name。每次呈现Button时,它都会登录到控制台。
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″ />
</>
)
}
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" /> </> ) }
在此示例中,无论何时单击任一按钮,您都会在控制台中看到:
// button1 rendered
// button2 rendered
// counter rendered // button1 rendered // button2 rendered
现在,如果我们应用useCallback
到我们的handleClick
函数并将我们的Button包装在 中React.memo
,我们可以看到useCallback
为我们提供了什么。React.memo
类似于useMemo
并且允许我们记忆一个组件。
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″ />
</>
)
}
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" /> </> ) }
现在,当我们单击任一按钮时,我们只会看到我们单击