React之memo,useMemo,useCallback

2,405 阅读3分钟

由来

我们做项目经常会碰到一种场景,父组件状态变更需要重新render,通常情况下会连带着所有子组件一起重新render。

当我们传递某个子组件props并没有改变,该子组件并不需要re-render

在之前的class组件中,我们可以使用 PureComponent 或 shouldComponentUpdate 来控制子组件是否需要渲染 于是为了处理函数组件渲染优化性能 memo、useMemo、useCallback 应运而生

React.memo

React.memo 仅检查 props 变更,默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

注意:但是函数中存在useStateuseReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染

import { ChangeEvent, memo,useState } from "react"
interface IProps{ 
    count?:number
    value?:string
}
const Child=(props:IProps):any=>{
    console.log('子组件render');
    return (
        <div>子组件count:{props.count}</div>
    )
};
//不使用memo
const ChildMemo=Child
//1 使用memo
//const ChildMemo=memo(Child)
//2 使用memo控制对比,第二个参数,true则re-render,false不re-render
// const ChildMemo=memo(Child,(prevProps, nextProps)=>{
//     if(prevProps.count===1){
//         return true
//     }else {
//         return false
//     }
// })
const Home =() =>{
    const [count, setCount] = useState(0)
    const [value, setValue] = useState('null');
    const addClick= () =>{ 
        setCount(count + 1)
    }
    const handleChange=(e: ChangeEvent<HTMLInputElement>)=>{
        setValue(e.target.value)
    }
    console.log('父组件render')
    return (
        <div>
        <p>父组件{count} times</p>
        <button onClick={addClick}>Click me</button>
        <input onChange={(e) => handleChange(e)} />
        <ChildMemo count={count}></ChildMemo>
        </div>
    );
}

不使用React.memo时,value值变化时,子组件还是会重新渲染,然而子组件没有依赖value值时并不需要渲染 Kapture 2022-01-15 at 15.22.31.gif 然后我们看下加上React.memo

Kapture 2022-01-15 at 15.06.45.gif 父组件改变value并不会影响子组件re-render 当我们memo第二个参数时,可以在加一下准确的判断条件进而控制,当我们加了prevProps.count===1返回true,当父组件count不等于1时,子组件已经不会再re-render

Kapture 2022-01-15 at 15.34.55.gif

useMemo 及useCallback

useMemo功能是判断组件中的函数逻辑是否重新执行,如果useMemo返回的是一个函数,则可以用useCallback省略顶层的函数,useCallbackuseMemo的变体,这两个hooks都返回缓存的值,useMemo返回缓存的变量、useCallback返回缓存的函数 下面看下useMemo如何优化的

import { ChangeEvent, memo,useMemo,useState } from "react"
interface IProps{ 
    onButtonClick?:()=>void,
    count?:number
    value?:string
}
const Child=(props:IProps):any=>{
    console.log('子组件render');
    return (
        <div>子组件count:{props.count}</div>
    )
};
const ChildMemo=memo(Child)
const Home =() =>{
    const [count, setCount] = useState(0)
    const [value, setValue] = useState('null');
    const addClick= () =>{ 
        setCount(count + 1)
    }
    const handleChange=(e: ChangeEvent<HTMLInputElement>)=>{
        setValue(e.target.value)
    }
    /**不使用useMemo */
    const childmemo=(()=>{
        let result = Math.random() * count;
        console.log('render-result')
        return result;
    })()
    /** 使用useMemo*/
    // const childmemo=useMemo(()=>{
    //     let result = Math.random() * count;
    //     console.log('render-result')
    //     return result;
    // },[count])
    console.log('父组件render')
    return (
        <div>
        <p>父组件{count} times <br/>使用useMemo的计算count {childmemo}</p>
        <button onClick={addClick}>Click me</button>
        <input onChange={(e) => handleChange(e)} />
        <ChildMemo count={count}></ChildMemo>
        </div>
    );
}

不使用useMemo这个时候我们看到childmemo的方法,在改变value的输入框,也会随着变化,但是我们childmemo依赖的是count,不应该变化

Kapture 2022-01-15 at 16.13.00.gif 当我们使用useMemo可以看到log只有count变化,值才跟着变化,这个类似vue里面computed Kapture 2022-01-15 at 16.15.14.gif

useCallbackuseMemo的延伸,再记忆一下useMemo返回缓存的变量、useCallback返回缓存的函数 我们把上面的修改一下,假如childmemo返回的是函数时

     const childmemo=useMemo(()=>{
        return ()=>{
            let result = Math.random() * count;
            console.log('render-result')
            return result;
        }
    },[count])

这个时候,就会出现useMemo不生效的,这个时候我们就可以使用useCallback修改后,省略上层函数,改写后

import { ChangeEvent, memo,useCallback,useEffect,useLayoutEffect,useMemo,useRef,useState } from "react"
interface IProps{ 
    callback?:()=>number,
    count?:number|undefined
    value?:string
}
const Child=(props:IProps):any=>{
    console.log('子组件render');
    let value:number| undefined=0
    useEffect(()=>{
       value=props.callback&&props.callback()
       console.log('callback更新了',value);
    },[props.callback])
    return (
        <div>子组件count:{props.count}
        </div>
    )
};
const ChildMemo=memo(Child)
const Home =() =>{
    const [count, setCount] = useState(0)
    const [value, setValue] = useState('null');
    const addClick= () =>{ 
        setCount(count + 1)
    }
    const handleChange=(e: ChangeEvent<HTMLInputElement>)=>{
        setValue(e.target.value)
    }
    const childmemo=useCallback(()=>{
        console.log('render-result')
        let result = Math.random() * count;
        return result
    },[count])
    console.log('父组件render')
    return (
        <div>
        <p>父组件{count} times</p>
        <button onClick={addClick}>Click me</button>
        <input onChange={(e) => handleChange(e)} />
        <ChildMemo count={count} callback={childmemo}></ChildMemo>
        </div>
    );
}

子组件接收,count值更改时,才会触发子组件props.callback的,去掉useCallback则修改value值时,childmemo也会执行。

以上都是个人记录理解的一个过程,如有问题望指正~