如果你已经使用 React 一段时间了,那么你应该了解到,在生产环境中开发人员会尽可能地优化其组件。在不必要的情况下,组件不应该被重新渲染。
函数组件中是怎么重新渲染的
几种方法是:
-
更改组件状态
-
更改组件道具
让我们看一个示例,由状态发生更改而重新渲染。
import React, {useState} from 'react';
function Demo() {
const [count, setCount] = useState(0);
const formatCounter = (e) => `value is ${e}`;
return (
<>
{formatCounter(count)}
<button onClick={() => setCount(prev => ++prev)}>+</button>
</>
);
}
当用户单击按钮时,我们调用 setCount()
方法,该方法将用新的 count
来重新渲染组件。在函数组件中,重新渲染意味着整个函数将再次运行。
-
因此,从顶部开始,
useState()
方法将运行。由于钩子的固有特性,这将返回更新的count
和缓存的setCount()
方法。 -
接下来,
formatCounter()
函数将被添加到内存中。 -
最后返回jsx,但在
<button/>
上,有一个onClick()
处理程序,它也会被添加到内存中。
每次组件重新渲染时,以上这些步骤都会发生,你需要以某种方式缓存 formatCounter()
和 onClick()
方法,这样它们就不会在每次重新渲染时都被添加到内存中。
天真的解决方案
因此,首先想到的是将 formatCounter()
和 onClick()
的处理程序移出组件。这样这些函数只会创建一次。
import React, {useState} from 'react';
const formatCounter = (e) => `value is ${e}`;
const onClick = (setCount) => setCount(prev => ++prev);
function Demo() {
const [count, setCount] = useState(0);
return (
<>
{formatCounter(count)}
<button onClick={onClick}>+</button>
</>
);
}
formatCounter()
函数很容易提取,它不依赖于任何特定于组件的变量或方法。
但是 onClick()
方法不是这样,它依赖 setCount()
方法!
没有简单的方法来提取这个方法。所以它需要存在于组件内部,并且需要缓存,这样就不会在每次重新渲染时都将其添加到内存中。
useCallback()
与
useState()
类似,React 提供了一个名为useCallback(Function, any[])
的钩子,简言之,数组中变量或函数发生变化(浅层检查),你将得到一个新函数,否则你将得到一个缓存函数。
import React, {useState, useCallback} from 'react';
const formatCounter = (e) => `value is ${e}`;
function Demo() {
const [count, setCount] = useState(0);
const onClick = useCallback(() => setCount(prev => ++prev), []);
return (
<>
{formatCounter(count)}
<button onClick={onClick}>+</button>
</>
);
}
当这个组件重新渲染时,onClick()
和 formatCounter()
都不会再次存进内存,缓缓举起大拇指。
看到这你会发现然并卵,接着看。
假设一个组件在树下有 20 个组件。如果这 20 个组件中所有未优化的函数和事件处理程序都将再次添加到内存中。这取决于特定的情况,当用户在屏幕上输入或单击时,可能会使交互变得卡顿。简言之,未优化的函数会持续累加。
依赖数组
React 提供了另一个类似于
useCallback()
的钩子,称为useMemo()
。它不接受回调函数,而是接受一个返回特定值的普通函数,这个函数需要始终有一个返回值。如果你发现该函数没有返回值,那么你可能需要改用useCallback()
。
经过一些实践,这两个钩子之间的区别变得很明显。
还有其它的优化吗?
function Demo() {
const name = ['a', 'b', 'c', 'd'];
return (
<div>{name}</div>
);
}
在上面的示例中,name
是一个数组,它在组件每次重新渲染时初始化。在这个示例中,天真的解决方案将把这个初始化从组件中抽离出去。但是,如果初始化依赖于组件本身,则应该使用 useMemo()
钩子并以这种方式对其进行优化。
现在我们还是使用 useMemo()
。
import React, {useMemo} from 'react';
function Demo() {
const name = useMemo(()=> ['a', 'b', 'c', 'd'],[]);
return (
<div>{name}</div>
);
}
现在让我们思考一个新的例子,其中初始化确实依赖于组件本身。
import React, {useState, useCallback, useMemo} from 'react';
function Demo(text) {
const [state, setState] = useState(true);
const onClick = useCallback(()=> setState(prev => !prev), []); const obj = {
a: state ? 1 : 2,
b: !!state,
};
return (
<>
<div>{text}{obj.a}</div>
<button onClick={onClick}>Change</button>
</>
);
}
这个例子有趣的是变量obj
完全依赖于 state
。如果状态改变了,则此对象也将改变。
细心一点你会发现,组件接受一个 text
的属性参数,该参数仅用于返回的 jsx中。
现在想象一下,text
发生了变化,会发生什么事?
好吧,组件会重新渲染对吗?name
会怎么样?
是的,你猜对了,它将被重新初始化并添加到内存中,但有必要这样做吗?我们需要重新初始化吗?当然不是,让我们使用 useMemo()
钩子来优化它。
import React, {useState, useCallback, useMemo} from 'react';
function Demo(text) {
const [state, setState] = useState(true);
const onClick = useCallback(()=> setState(prev => !prev), []); const obj = useMemo(()=>({
a: state ? 1 : 2,
b: !!state,
}, []);
return (
<>
<div>{text}{obj.a}</div>
<button onClick={onClick}>Change</button>
</>
);
}
如此,obj
变量现在已优化并准备就绪。哦,等等...
如果状态发生变化(通过单击按钮),您认为优化的obj
会拿到正确的值吗?
当然不会,为什么呢?
如果提供给这些优化钩子的内容中有一些变量/函数/对象/数组,需要是最新的值,那么需要将其作为依赖项提供。
我需要 state
的最新值,以使name
是正确的。如果不将 state
变量作为依赖项提供,name
将始终是:
{
a: 1,
b: true,
}
这是刚开始 state
变量初始化为 true
之后初始化的变量。在那之后,它永远不会改变。
在本例中,我们希望它在 state
更改时更新。因此,我们只提供 state
作为依赖项,一切都很好。
import React, {useState, useCallback, useMemo} from 'react';
function Demo(text) {
const [state, setState] = useState(true);
const onClick = useCallback(()=> setState(prev => !prev), []); const obj = useMemo(()=>({
a: state ? 1 : 2,
b: !!state,
}, [state]);
return (
<>
<div>{text}{obj.a}</div>
<button onClick={onClick}>Change</button>
</>
);
}
所以现在如果不调用setState()
函数,那么obj
将始终返回缓存的值,并且不会每次都将其添加到内存中,比如 text
参数更改时。
总结
自己总结。