前言:React框架中,当组件的props或state发生变化时,会重新渲染组件,实际开发中会遇到不必要的重新渲染场景。这里的memo(),useMemo(),useCallback()都是应用在函数组件中。调理不够清楚,希望你能够一步一步看。
React.memo()
和类组件中React.PureComponent()
很相似,它可以帮助我们控制什么时候重新渲染组件,它适用于函数组件,但不适用于 class 组件。如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
下面看一个很简单的例子:
import React, { useState } from "react";
//子组件
const ChildCom = () => {
console.log('render child component');
return (
<div>ChildComponent</div>
)
};
//父组件
const ParaentCom = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>点击加1</button>
<div>点击次数:{count}</div>
<ChildCom />
</div>
)
}
export default ParaentCom;
子组件中有console语句,在函数组件中,每次重新渲染都会重新打印输出。我们点击父组件中按钮,会修改count的值,进而导致父组件重新渲染,但是我们发现即使子组件的props和state没有变化,但控制台显示子组件也重新渲染了。但是我们这里期望的是:子组件的props和state没有变化时,即使父组件渲染,子组件也不需要重新渲染。
解决方法:使用React.memo()将子组件进行包裹
import React, { memo,useState } from "react";
//子组件
const ChildCom = memo(() => {
console.log('render child component');
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>点击加1</button>
<div>点击次数:{count}</div>
<ChildCom />
</div>
)
}
export default ParaentCom;
子组件使用React.memo包裹之后,我们多次点击按钮,发现子组件没有重新渲染。
上面的例子只是最简单的父组件没有向子组件传递props的情况,如果传递props时,我们怎么控制子组件不重复渲染或者只让某些props中的属性变化时子组件才重新渲染,这就需要用到 React.useMemo()。
useMemo 有两个参数:
看一个简单例子
import React, { memo,useState } from "react";
//子组件
const ChildCom = memo(({ number }) => {
console.log('render child component', number);
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={increment}>点击加1</button>
<div>点击次数:{count}</div>
<ChildCom number={count} />
</div>
)
}
export default ParaentCom;
如果props不改变,只使用memo()就可以控制子组件不重新渲染。上面是当传入props的值改变时,我们发现子组件进行了重新渲染,但是我们现在的期望是,即使props的值改变,子组件也不要重新渲染。
解决方法,看代码:
import React, { memo, useMemo,useState } from "react";
//子组件
const ChildCom = memo(({ number }) => {
console.log('render child component', number);
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const number = useMemo(() => (count), []);
return (
<div>
<button onClick={increment}>点击加1</button>
<div>点击次数:{count}</div>
<ChildCom number={number} />
</div>
)
}
export default ParaentCom;
但是,下面一个期望是,控制props中的某些属性改变,也就是复杂的props,可以使子组件重新渲染,这里就需要useMemo的第二个参数来发挥作用了。
看代码:
import React, { memo, useMemo, useState } from "react";
//子组件
const ChildCom = memo(({ number }) => {
console.log('render child component', number);
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const changeName = () => setName(name + 1);
const changeAge = () => setAge(age + 1);
const [name, setName] = useState('tom');
const [age, setAge] = useState(20);
const number = useMemo(() => ({ name, age }), [age]);
return (
<div>
<button onClick={changeAge}>点击修改年龄</button>
<button onClick={changeName}>点击修改姓名</button>
<div>姓名:{name} 年龄:{age}</div>
<ChildCom number={number} />
</div>
)
}
export default ParaentCom;
点击修改姓名时:
点击修改年龄时:
从上面两个截图中可以看出,可以这样理解,使用useMemo()方法监听age,从而让我们可以控制子组件什么时候进行重新渲染。但是props如果是方法的话,子组件依然会重新渲染,这里我们就需要useCallback()钩子。
先看一段代码:
import React, { memo, useState } from "react";
//子组件
const ChildCom = memo(() => {
console.log('render child component');
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const [age, setAge] = useState(20);
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const changeAge = () => setAge(age + 1);
// const changeAge = useCallback(() => setAge(age + 1), [age])
return (
<div>
<button onClick={increment}>点击加1</button>
<div> 点击次数:{count}</div>
<ChildCom onClick={changeAge} />
</div>
)
}
export default ParaentCom;
这里由于父组件重新渲染,导致传递的方法也重新渲染,也就是props又重新渲染了,所以子组件又重新渲染了。这种情况下,我们依然不想让子组件重新渲染。
解决方法:
import React, { memo, useCallback, useState } from "react";
//子组件
const ChildCom = memo(() => {
console.log('render child component');
return (
<div>ChildComponent</div>
)
});
//父组件
const ParaentCom = () => {
const [age, setAge] = useState(20);
const [count, setCount] = useState(0);
const increment = () => setCount(count + 1);
const changeAge = useCallback(() => setAge(age + 1), [])
return (
<div>
<button onClick={increment}>点击加1</button>
<div> 点击次数:{count}</div>
<ChildCom onClick={changeAge} />
</div>
)
}
export default ParaentCom;
此时点击父组件按钮,控制台就不会打印子组件被渲染的信息了。可以理解为:useCallback()起到了缓存的作用,即使父组件渲染了,useCallback()包裹的函数也不会重新生成,会返回上一次的函数引用。useCallback()钩子的第二个参数,和useMemo,useEffect用法都是一样的,这里就不多介绍。