React 中的记忆化:简单介绍
您可以使用多种优化技术来提升 React 应用程序的整体性能。其中之一就是 memoization。在本教程中,您将了解什么是 memoization,以及如何在 React 中使用 memoization 来优化您的 React 应用程序。
记忆变得简单
记忆化是编程中使用的优化技术之一。它可以通过避免不必要的计算来节省时间和资源。当计算结果与上次编译的结果相同时,无需进行计算。
让我们举一个简单的例子。假设你有一个函数,它返回给定数字的阶乘。通常,这个函数会对你输入的每个数字进行一次计算。这有必要吗?例如,假设你用同一个数字运行这个函数两到三次。
在这种情况下,是否有必要运行整个计算来返回该函数过去已经见过的值?不需要。为了避免这种情况,你可以创建一个缓存并修改函数。每次函数运行时,它都会先查看缓存。
如果您传递给函数的数字已经在缓存中,则无需进行任何计算。阶乘函数只需返回该数字的已知结果即可。如果该数字不在缓存中,阶乘函数可以执行其工作,计算阶乘并将其添加到缓存中。
// Create cache:
let cache = [1]
// Create memoized factorial function:
function getFactorialMemoized(key) {
if (!cache[key]) {
// Add new value to cache:
cache[key] = key * getFactorialMemoized(key - 1)
} else {
// Return cached value:
console.log('cache hit:', key)
}
// Return result
return cache[key]
}
getFactorialMemoized(6)
getFactorialMemoized(6)
这个例子演示了记忆化的基本原理。你计算一些值,并存储它们,将它们记忆起来,以备后用。如果将来某个时候你需要获取其中一个值,你无需再次计算它们。相反,你可以从你的存储(也就是缓存)中获取它们。
你可能已经猜到了,这项技术可以显著提升性能。通常情况下,直接返回某个值比计算某个值要快得多,而且资源占用也更少。这听起来很棒,但是如何在 React 中使用 memoization 呢?
React 中的记忆化
好消息是,React 提供了开箱即用的内置记忆工具。这意味着你无需添加任何额外的依赖项。你唯一需要的依赖项是react和 react-dom。React 目前提供的记忆工具有三个:memo()
、useMemo()
和useCallback()
。
备忘录
React 中第一个用于记忆的工具是名为 的高阶组件memo()
。高阶组件的作用是接受一个 React 组件并返回新的。与 有memo()
一个重要的区别。返回的新组件也会被记忆。
这意味着,除非需要更新,否则 React 不会重新渲染这个已记忆的组件。也就是说,只要组件的 props 保持不变,React 就会跳过重新渲染已记忆的组件,而是继续复用上次渲染的结果。
当 React 检测到某个组件的 prop 发生变化时,它会重新渲染该组件。这是为了确保 UI 保持最新和同步。说到memo()
,有两件重要的事情需要提及。
// Import memo
import { memo } from 'react'
// Component without memo:
export const App = () => {
return (
<div>
<h1>This is a normal component</h1>
</div>
)
}
// Component wrapped with memo:
export const App = memo(() => {
return (
<div>
<h1>This is a memoized component</h1>
</div>
)
})
地方州
首先,React 只会监听 props 的变化,而不会监听组件内部逻辑的变化。它也不会阻止这些变化重新渲染组件。例如,如果组件有自己的本地状态,就会发生这种变化。
当本地状态发生变化时,组件仍会重新渲染。这是设计使然,旨在确保 UI 和日期同步。这也适用于连接到提供程序或 Redux Store 的组件。这些数据实体的更改将导致与其连接的组件重新渲染。
让我们看一个简单的例子。假设你有一个跟踪计数的组件。它会渲染当前计数和一个按钮,使计数加 1。即使组件本身已记忆,每次点击按钮都会导致重新渲染。
需要记住的是,这不是一个 bug,而是一个特性。React 会重新渲染组件,以使渲染的计数值与组件本地状态中的数据保持同步。如果不重新渲染,渲染的数字将停留在 0。
// Import memo and useState:
import { memo, useState } from 'react'
export const App = memo(() => {
// Create local state:
const [count, setCount] = useState(0)
// This will log on every re-render:
console.log('Render')
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
return (
<div>
<h1>Current count: {count}</h1>
<button onClick={onCountClick}>Click me</button>
</div>
)
})
浅显的比较
第二点是,React 只对 memoized 组件的 props 进行浅层比较。如果你传递的 props 数据比原始数据类型更复杂,这可能不够。在这种情况下,memo()
HOC 还允许将自定义的比较函数作为第二个参数传递。
这个自定义比较函数有两个参数:previous 和 next 属性。在这个函数中,你可以执行任何你需要的自定义比较逻辑。
// Import memo and lodash:
import { memo } from 'react'
import { isEqual } from 'lodash'
// Create custom comparison function:
function isEqual(prevProps, nextProps) {
// Return result of some custom comparison:
return isEqual(prevProps, nextProps)
}
// Component wrapped with memo:
export const App = memo(() => {
return (
<div>
<h1>This is a memoized component</h1>
</div>
)
}, isEqual) // Pass custom comparison function
使用备忘录
在 React 中,第二个有助于记忆的工具是 React hook useMemo()。与 不同memo()
,该useMemo
hook 允许你执行一些计算并记忆其结果。这样,只要它监视的输入保持不变,useMemo()
它就会返回缓存的结果,从而避免不必要的计算。
一个简单的例子
例如,假设某个组件通过 props 获取一个数字。然后它获取这个数字并计算它的阶乘。这正是我们想要通过记忆化来优化的复杂计算。该组件还有一个本地状态。它可以是我们已经尝试过的计数跟踪器。
我们将添加一个计算阶乘的函数,并使用该函数计算阶乘,并将结果赋给常规变量。会发生什么?阶乘将在组件安装时计算。问题是,当我们点击计数按钮并增加计数时,阶乘也会被计算。
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = ({ number }) => {
// Create local state:
const [count, setCount] = useState(0)
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Create factorial function:
const getFactorial = (num) => {
// Print log when function runs:
console.log('count factorial')
// Return the factorial:
return num === 1 ? num : num * getFactorial(num - 1)
}
// Calculate factorial for number prop:
const factorial = getFactorial(number)
// THIS ^ is the problem.
// This variable will be re-assigned,
// and factorial re-calculated on every re-render,
// every time we click the button to increment count.
return (
<div>
<div>Count: {count}</div>
<div>Factorial: {factorial}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
在上面的例子中,我们可以看到 factorial 被重新计算了,因为每次点击按钮时,里面的日志getFactorial()
都会打印在控制台中。这意味着每次点击按钮时,getFactorial()
都会执行该函数,即使 props 中的数字相同。
一个简单的解决方案
我们可以借助useMemo()
钩子快速解决这个问题。我们只需getFactorial()
用 包装函数调用即可useMemo()
。这意味着我们将使用钩子factorial
为变量赋值useMemo()
,并将getFactorial()
函数传递给钩子。
我们还应该确保当通过 props 传递的数字发生变化时,阶乘会被重新计算。为此,我们将此 props 指定为我们想要在useMemo()
hook 依赖数组中监视的依赖项。
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = ({ number }) => {
// Create local state:
const [count, setCount] = useState(0)
// Create button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Create factorial function:
const getFactorial = (num) => {
// Print log when function runs:
console.log('count factorial')
// Return the factorial:
return num === 1 ? num : num * getFactorial(num - 1)
}
// Calculate and memoize factorial for number prop:
const factorial = useMemo(() => getFactorial(number), [number])
// 1. Wrap the getFactorial() function with useMemo
// 2. Add the "number" to dependency array ("[number]") to tell React it should watch for changes of this prop
return (
<div>
<div>Count: {count}</div>
<div>Factorial: {factorial}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
得益于这个简单的更改,我们可以避免不必要的计算,避免降低 React 应用的速度。这样,我们可以记住任何需要的计算。我们还可以使用useMemo()
多次操作来确保重新渲染时的计算量最小化。
// Import useState and useMemo:
import { useState, useMemo } from 'react'
export const App = () => {
// Add state to force re-render
const [count, setCount] = useState(0)
// Add button handler:
const onCountClick = () => setCount((prevCount) => ++prevCount)
// Add some dummy data and memoize them:
const users = useMemo(
() => [
{
full_name: 'Drucy Dolbey',
gender: 'Male',
},
{
full_name: 'Ewart Sargint',
gender: 'Male',
},
{
full_name: 'Tabbi Klugel',
gender: 'Female',
},
{
full_name: 'Cliff Grunguer',
gender: 'Male',
},
{
full_name: 'Roland Ruit',
gender: 'Male',
},
{
full_name: 'Shayla Mammatt',
gender: 'Female',
},
{
full_name: 'Inesita Eborall',
gender: 'Female',
},
{
full_name: 'Kean Smorthit',
gender: 'Male',
},
{
full_name: 'Celestine Bickerstaff',
gender: 'Female',
},
],
[]
)
// Count female users and memoize the result:
const femaleUsersCount = useMemo(
() =>
users.reduce((acc, cur) => {
console.log('Invoke reduce')
return acc + (cur.gender === 'Female' ? 1 : 0)
}, 0),
[users]
)
return (
<div>
<div>Users count: {femaleUsersCount}</div>
<button onClick={onCountClick}>Click me</button>
</div>
)
}
在上面的例子中,仅仅记住赋值的结果是不够的。我们还femaleUsersCount
必须记住。否则,每次组件重新渲染时,变量都会被重新赋值。这也会触发。这意味着实际上什么都没有被记住。users
users
useMemo()
femaleUsersCount
当我们进行记忆化时,users
我们会阻止它被重新赋值。这将防止对 进行不必要的更改users
,从而避免对 进行不必要的更改femaleUsersCount
。因此,只有count
会改变。实际上,onCountClick()
也会被重新创建。这就引出了 React 中最后一个用于记忆化的工具。
使用回调
memo()
在 React 中,我们可以利用memoization做很多事情,useMemo()
以避免各种不必要的计算。还有一个问题我们还没解决。每次组件重新渲染时,它都会重新创建所有本地函数。这是一把双刃剑。
重新创建函数的两个问题
它是一把双刃剑,因为它可能导致两个问题。首先,你在组件中声明的所有函数在每次渲染时都会被重新创建。这可能会产生重大影响,具体取决于你通常有多少个函数。第二个问题可能会导致更多问题。
举个简单的例子。假设你有一个父组件和一个子组件。父组件会创建一个本地状态和一个函数。该函数也会通过 props 传递给子组件,以便子组件可以使用。问题来了?你还记得memo()
浅比较吗?
问题是,当你将函数传递给组件时,你传递的是复杂值,而不是原始值。React 的浅层比较会在这里失败。它会告诉你值不同,并且即使值相同也会重新渲染组件。在我们的例子中,值就是函数。
当父组件重新渲染时,它也会重新创建传递给子组件的函数。当重新创建的函数被传递时,React 无法识别该函数,尽管它是新创建的,但实际上它与前一个函数相同。
这样做的结果是子组件也会重新渲染。无论你memo()
是否使用,这都会发生。
// Child component:
import { memo } from 'react'
export const CountChild = memo((props) => {
console.log('CountBox render')
return <button onClick={props.onChildBtnClick}>Click me as well</button>
})
// Parent component:
import { useState, memo, useCallback } from 'react'
// Import child component
import { CountChild } from './countChild'
export const App = memo(() => {
// Add state to force re-render
const [count, setCount] = useState(0)
// Add button handler:
const onCountClick = () => {
setCount((prevCount) => ++prevCount)
}
return (
<div>
<div>count: {count}</div>
<button onClick={onCountClick}>Click me</button>
<CountBox onChildBtnClick={onCountClick} />
</div>
)
})
避免通过 props 传递函数导致的重新渲染
避免这种情况的方法是使用useCallback()钩子。我们不必像平常一样声明一个函数,而是将其作为回调传递给useCallback()
钩子并将其赋值给一个变量。这样做,再加上正确设置数组依赖项,就能确保该函数仅在必要时重新创建。
这意味着仅当依赖项之一发生变化时才会执行。当重新渲染发生时,如果依赖项没有发生变化,React 将使用函数的缓存版本,而不是重新创建它。React 返回函数的缓存版本还可以防止子组件不必要的重新渲染。
这是因为 React 知道该函数已被缓存,因此保持不变。所以,除非其他 prop 发生变化,否则无需重新渲染子组件。
// Child component:
import { memo } from 'react'
export const CountChild = memo((props) => {
console.log('CountBox render')
return <button onClick={props.onChildBtnClick}>Click me as well</button>
})
// Parent component:
import { useState, memo, useCallback } from 'react'
// Import child component
import { CountChild } from './countChild'
export const App = memo(() => {
// Add state to force re-render
const [count, setCount] = useState(0)
// CHANGE: Memoize the button handler:
const onCountClick = useCallback(() => {
setCount((prevCount) => ++prevCount)
}, []) // No dependency is needed
return (
<div>
<div>count: {count}</div>
<button onClick={onCountClick}>Click me</button>
<CountBox onChildBtnClick={onCountClick} />
</div>
)
})
结论:React 中的记忆化
得益于memo()
, React 中的 memoization 变得非常简单。借助这些工具,useMemo()
我们useCallback()
可以让 React 应用程序更快、更高效。希望本教程能帮助您理解什么是 memoization,以及如何在 React 中使用 memoization 来优化您的 React 应用程序。