如果你已经使用 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 参数更改时。
总结
自己总结。