一 React.memo和useMemo
1.memo的作用
当父组件的数据变化时,代码会重新执行一遍,子组件数据没有变化也会执行,这个时候可以使用memo将子组件封装起来,让子组件的数据只在发生改变时才会执行。
我们先来看一个不用memo的例子:
改变count和num的值都会触发子组件的重新渲染
import React, {
useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'
const Test2 = () => {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
const changeCount = () => {
setCount(count=>count+1)
}
const changeNum = () => {
setNum(num=>num+1)
}
return (
<>
<div onClick={
changeCount}>
{
count}
</div>
<div onClick={
changeNum}>
{
num}
</div>
<Test4 num={
num} />
</>
);
}
export default Test2;
import React, {
useEffect, useRef, useState } from 'react';
const Test4 = (props) => {
const {
num} = props
console.log('children-----')
return (
<div>
{
num}
</div>
);
}
export default Test4;
如果我们用memo包裹子组件,就能控制子组件只在num发生变化时进行渲染。
export default React.memo(Test4);
但是这里有一个bug,如果在子组件上还自定义了函数,那么改变count的值依然能使子组件re-render。
import React, {
useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'
const Test2 = () => {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
const changeCount = () => {
setCount(count=>count+1)
}
const changeNum = () => {
setNum(num=>num+1)
}
const clickNum = () => {
console.log('click-----')
}
return (
<>
<div onClick={
changeCount}>
{
count}
</div>
<div onClick={
changeNum}>
{
num}
</div>
<Test4 num={
num} clickNum={
clickNum} />
</>
);
}
export default Test2;
import React, {
useEffect, useRef, useState } from 'react';
const Test4 = (props) => {
const {
num, clickNum} = props
console.log('children-----')
return (
<div onClick={
clickNum}>
{
num}
</div>
);
}
export default React.memo(Test4);
主要原因就是当count值改变时,重新执行代码,clickNum空函数的地址会发生变化,从而导致子组件重新渲染。
总结:
memo的作用就相当于函数组件的PureComponent。
PureComponent会根据props和state的浅对比来实现shouldComponentUpate()。但是如果在PureComponent中包含了比较复杂的数据结构,可能会因为深层的数据不一致而产生错误的否定判断,导致界面无法更新。
memo也一样,如果函数组件被React.memo包裹,且其实现中拥有useState或useContext的Hook,当context发生变化时,它仍会重新渲染。且memo是浅比较,只要对象的内存地址不变,无论值怎么变化都不会触发render。
2.useMemo的作用
(1)解决因函数更新而渲染自己的问题,就可以使用useMemo,使用它将函数重新封装。
useMemo(fn,array)
监听变量,第一个参数是函数,第二个参数是依赖,只有依赖变化时才会重新计算函数。
上面的例子,我们可以对clickNum函数做如下修改,只有num发生变化时才会触发函数的执行,才会触发子组件的re-render。
const clickNum = useMemo(() => {
return ()=>{
console.log('num changed')
}
},[num])
这里也可以换成useCallback,效果是一样的:
const clickNum = useCallback(() => {
console.log('num changed')
},[num])
这样改变count的值就不会触发子组件的更新。
实际上通过useMemo或useCallback将函数包装一层后,会根据后面的依赖项决定是否返回一个新的函数,函数内部作用域也随之更新。
(2)如果组件中有复杂计算的函数,应该使用useMemo。因为useCallback缓存函数的引用,useMemo缓存计算数据的值。useMemo是避免在每次渲染时都进行高开销的计算的优化策略。
import React, {
useEffect, useCallback, useState, useMemo } from 'react';
import Test4 from './Test4'
const Test2 = () => {
const [count, setCount] = useState(0)
const [num, setNum] = useState(0)
const changeCount = () => {
setCount(count=>count+1)
}
const changeNum = () => {
setNum(num=>num+1)
}
const doubleNum = useMemo(() => {
return 2*num
},[num])
return (
<>
<div onClick={
changeCount}>
{
count}
</div>
<div onClick={
changeNum}>
{
num}
</div>
<div>{
doubleNum}</div>
</>
);
}
export default Test2;
如果doubleNum是一个计算过程复杂或者开销较大的函数,可以用useMemo获取缓存的值,避免函数的多次执行。
二 useCallback
useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。
官方的等式:useCallback(fn,deps) 相当于 useMemo(()=>fn,deps)。
我们可以将useCallback结合useEffect,实现按需加载。通过hooks的配合,使得函数不再仅仅是一个方法,而是可以作为一个值参与到应用的数据流中。
const Parent = () => {
const [ count, setCount ] = useState(0);
const [ step, setStep ] = useState(0);
const fetchData = useCallback(() => {
const url = "https://count=" + count + "&step=" + step;
}, [count, step])
return (
<Child fetchData={
fetchData} />
)
}
const Child = (props) => {
useEffect(() => {
props.fetchData()
}, [props.fetchData])
return (
// ...
)
}
下面看一个useContext的频繁更新所带来的问题
const [text, setText] = useState('');
const handleSubmit = useCallback(() => {
// ...
}, [text]);
return (
<form>
<input value={
text} onChange={
(e) => setText(e.target.value)} />
<OtherForm onSubmit={
handleSubmit} />
</form>
);
text的频繁更新会带来handleSubmit的频繁执行,很耗性能。这里我们可以通过useRef进行优化。
const textRef = useRef('');
const [text, setText] = useState('');
const handleSubmit = useCallback(() => {
console.log(textRef.current);
// ...
}, [textRef]);
return (
<form>
<input value={
text} onChange={
(e) => {
const {
value } = e.target;
setText(value)
textRef.current = value;
}} />
<OtherForm onSubmit={
handleSubmit} />
</form>
);
useCallback的依赖是只比较值的, 如果是对象, 就是只比较引用。而textRef是一直存在不会销毁的跨生命周期对象, 引用一直不变, 相当于, useCallback的依赖为[]。使用 useRef 可以生成一个变量让其在组件每个生命周期内都能访问到,且 handleSubmit 并不会因为 text 的更新而更新,也就不会让 OtherForm 多次渲染。
三 关于useMemo和useCallback
1.共同点:
仅依赖数据发生变化, 才会重新计算结果,也就是起到缓存的作用。
2.区别:
(1)useMemo计算结果是return回来的值, 主要用于缓存计算结果的值,应用场景如:需要计算的状态
(2)useCallback计算结果是函数, 主要用于缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个state的变化会使整个组件都被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。